dsh-my-guardian 0.3.6 → 0.4.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/CHANGELOG.md +8 -0
- package/README.md +10 -0
- package/lib/api.js +3 -0
- package/lib/client.js +110 -0
- package/lib/dep-precheck.js +3 -1
- package/lib/index.js +35 -1
- package/lib/parts/styles.part.js +27 -0
- package/lib/parts/util.part.js +23 -0
- package/lib/parts/view.part.js +60 -0
- package/lib/startup-check.js +228 -0
- package/lib/state.js +20 -0
- package/package.json +2 -5
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.4.0] - 2026-09-07
|
|
9
|
+
|
|
10
|
+
### 变更
|
|
11
|
+
|
|
12
|
+
- feat(observability): #155 插件状态查询聚合——统一 status-query 事件 + /plugin-status API (#170)
|
|
13
|
+
- chore(plugins): #165 清理失效的 dsh.client.inject 声明(13 插件) (#167)
|
|
14
|
+
- feat(guard): #144 启动名册静态预检——startup-issues.json + 面板置顶 + 不阻断启动 (#151)
|
|
15
|
+
|
|
8
16
|
## [0.3.6] - 2026-09-04
|
|
9
17
|
|
|
10
18
|
### 变更
|
package/README.md
CHANGED
|
@@ -86,6 +86,16 @@ dsh plugin --profile web add link:<仓库路径>/plugins/dsh-my-guardian
|
|
|
86
86
|
|
|
87
87
|
失败条目额外带**失败类型徽标**(依赖缺失 / 代码错误 / 其他),依赖缺失时并展示安装建议命令(如 `dsh plugin add dsh-shared`)。
|
|
88
88
|
|
|
89
|
+
### 启动名册静态预检(issue #144)
|
|
90
|
+
|
|
91
|
+
候选区只兜住"走候选区"的插件;**直接写进启动名册(`cordis.patch.yml` / profile / bundles)的插件仍由 DSH 启动时 all-or-nothing 加载**。守护在每次启动时对名册做静态预检(不实际加载插件):
|
|
92
|
+
|
|
93
|
+
- **包可解析**:名册中 `dsh-*` 插件的包必须在 profile node_modules 中存在
|
|
94
|
+
- **peerDependencies 满足**:复用候选挂载的依赖预检
|
|
95
|
+
- **无重复 entry id**:名册中同一 id 出现多次(加载时 `EntryGroup.update` 会抛 `duplicate loader entry id`)
|
|
96
|
+
|
|
97
|
+
发现的问题写入 `$DSH_HOME/guardian/startup-issues.json`(原子写,名册健康也写空报告 + 检查时间),面板「最近事件」顶部**置顶**展示启动区问题,每条给出修复命令(`dsh plugin add ...`)与移除提示。**预检失败不阻断启动**——只记录、只告警,最坏情况是为启动失败留下排查记录。
|
|
98
|
+
|
|
89
99
|
### 效果截图(真实 DSH 实例验证)
|
|
90
100
|
|
|
91
101
|
侧边栏"插件守护"诊断面板(独立 3081 端口隔离 DSH 实例实测):
|
package/lib/api.js
CHANGED
|
@@ -191,5 +191,8 @@ function snapshot(shared) {
|
|
|
191
191
|
staged: stagedList,
|
|
192
192
|
promoted: promotedList,
|
|
193
193
|
events: shared.state.events.slice(-10),
|
|
194
|
+
// startup-roster pre-check report (issue #144): empty array = healthy
|
|
195
|
+
startupIssues: Array.isArray(shared.startupIssues) ? shared.startupIssues : [],
|
|
196
|
+
startupCheckedAt: typeof shared.startupCheckedAt === 'number' ? shared.startupCheckedAt : null,
|
|
194
197
|
}
|
|
195
198
|
}
|
package/lib/client.js
CHANGED
|
@@ -182,6 +182,33 @@ const STYLES = `
|
|
|
182
182
|
.dsh-my-guardian-event-message { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
|
|
183
183
|
font:var(--dsw-font-xxs-12); color:var(--dsw-alias-label-secondary); }
|
|
184
184
|
.dsh-my-guardian-event-time { flex:none; font:var(--dsw-font-xxxs-11); color:var(--dsw-alias-label-tertiary); white-space:nowrap; }
|
|
185
|
+
/* ── startup-roster issues (issue #144): pinned block above the list ────── */
|
|
186
|
+
.dsh-my-guardian-startup-issues { display:flex; flex-direction:column; gap:2px; margin-top:4px; padding:6px 8px; border-radius:8px;
|
|
187
|
+
border:1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 45%, transparent);
|
|
188
|
+
background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 6%, transparent);
|
|
189
|
+
animation:dsh-my-guardian-row-in 150ms var(--ds-ease-in-out); }
|
|
190
|
+
.dsh-my-guardian-startup-issues-title { display:flex; align-items:center; gap:5px; padding:0 2px 3px;
|
|
191
|
+
font:var(--dsw-font-xxs-strong-12); color:var(--dsw-alias-state-error-primary); }
|
|
192
|
+
.dsh-my-guardian-startup-issues-title svg { display:block; flex:none; }
|
|
193
|
+
.dsh-my-guardian-startup-issues-count { display:inline-flex; align-items:center; justify-content:center; min-width:17px; height:17px; padding:0 5px; border-radius:4px;
|
|
194
|
+
font:var(--dsw-font-xxxs-strong-11); color:var(--dsw-alias-label-primary-foreground); background:var(--dsw-alias-state-error-primary); }
|
|
195
|
+
.dsh-my-guardian-startup-issues-time { flex:1; text-align:right; font:var(--dsw-font-xxxs-11); color:var(--dsw-alias-label-tertiary); }
|
|
196
|
+
.dsh-my-guardian-startup-issue { display:flex; flex-direction:column; gap:2px; padding:5px 6px; border-radius:6px;
|
|
197
|
+
border:1px solid var(--dsw-alias-border-l1); background:transparent; }
|
|
198
|
+
.dsh-my-guardian-startup-issue:hover { background:var(--dsw-alias-interactive-bg-hover); }
|
|
199
|
+
.dsh-my-guardian-startup-issue-head { display:flex; align-items:center; gap:6px; min-width:0; }
|
|
200
|
+
.dsh-my-guardian-startup-issue-name { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
|
|
201
|
+
font:var(--dsw-font-s-strong-14); color:var(--dsw-alias-label-primary); }
|
|
202
|
+
.dsh-my-guardian-startup-issue-badge-unresolvable { color:var(--dsw-alias-state-error-primary); background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 14%, transparent); }
|
|
203
|
+
.dsh-my-guardian-startup-issue-badge-duplicate-id { color:var(--dsw-alias-state-error-primary); background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 14%, transparent); }
|
|
204
|
+
.dsh-my-guardian-startup-issue-badge-dependency { color:var(--dsw-alias-state-warn-primary); background:color-mix(in srgb, var(--dsw-alias-state-warn-primary) 16%, transparent); }
|
|
205
|
+
.dsh-my-guardian-startup-issue-message { font:var(--dsw-font-xxs-12); color:var(--dsw-alias-label-secondary); line-height:1.6; }
|
|
206
|
+
.dsh-my-guardian-startup-issue-line { display:flex; align-items:flex-start; gap:5px; padding:2px 0;
|
|
207
|
+
font:var(--dsw-font-xxxs-11); color:var(--dsw-alias-label-tertiary); line-height:1.6; min-width:0; }
|
|
208
|
+
.dsh-my-guardian-startup-issue-label { flex:none; color:var(--dsw-alias-label-tertiary); }
|
|
209
|
+
.dsh-my-guardian-startup-issue-line code { font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size:var(--dsw-font-xxxs-11);
|
|
210
|
+
color:var(--dsw-alias-state-success-primary); word-break:break-all; }
|
|
211
|
+
.dsh-my-guardian-startup-issue-remove { min-width:0; word-break:break-all; }
|
|
185
212
|
@keyframes dsh-my-guardian-row-in { from { opacity:0; transform:translateY(1px); } to { opacity:1; transform:none; } }
|
|
186
213
|
@keyframes dsh-my-guardian-spin { to { transform:rotate(360deg); } }
|
|
187
214
|
`
|
|
@@ -237,6 +264,13 @@ const strings = {
|
|
|
237
264
|
failureCode: () => (isZh() ? '代码错误' : 'Code error'),
|
|
238
265
|
failureOther: () => (isZh() ? '其他' : 'Other'),
|
|
239
266
|
installHint: () => (isZh() ? '安装建议' : 'Install'),
|
|
267
|
+
// ── startup-roster issues (issue #144) ─────────────────────────────────
|
|
268
|
+
startupIssues: () => (isZh() ? '启动区问题' : 'Startup roster issues'),
|
|
269
|
+
startupIssueFix: () => (isZh() ? '修复' : 'Fix'),
|
|
270
|
+
startupIssueRemove: () => (isZh() ? '移除' : 'Remove'),
|
|
271
|
+
startupIssueUnresolvable: () => (isZh() ? '包不可解析' : 'Unresolvable'),
|
|
272
|
+
startupIssueDependency: () => (isZh() ? '依赖缺失' : 'Dependency'),
|
|
273
|
+
startupIssueDuplicate: () => (isZh() ? '重复 id' : 'Duplicate id'),
|
|
240
274
|
}
|
|
241
275
|
|
|
242
276
|
// ── api ───────────────────────────────────────────────────────────────
|
|
@@ -304,6 +338,7 @@ const EVENT_LABELS = {
|
|
|
304
338
|
safe: () => (isZh() ? '安全模式' : 'Safe mode'),
|
|
305
339
|
'safe-mode': () => (isZh() ? '安全模式' : 'Safe mode'),
|
|
306
340
|
skip: () => (isZh() ? '跳过' : 'Skipped'),
|
|
341
|
+
'startup-issue': () => (isZh() ? '启动区问题' : 'Startup issue'),
|
|
307
342
|
}
|
|
308
343
|
|
|
309
344
|
/** Badge color variant for an event type; unknown types fall back to the
|
|
@@ -316,6 +351,7 @@ function eventVariant(type) {
|
|
|
316
351
|
return 'accent'
|
|
317
352
|
case 'quarantine':
|
|
318
353
|
case 'update-failed':
|
|
354
|
+
case 'startup-issue':
|
|
319
355
|
return 'danger'
|
|
320
356
|
case 'freeze':
|
|
321
357
|
case 'safe':
|
|
@@ -326,6 +362,20 @@ function eventVariant(type) {
|
|
|
326
362
|
}
|
|
327
363
|
}
|
|
328
364
|
|
|
365
|
+
/** Startup-issue badge label (issue #144): unresolvable / dependency / dup. */
|
|
366
|
+
function startupIssueLabel(type) {
|
|
367
|
+
switch (type) {
|
|
368
|
+
case 'unresolvable':
|
|
369
|
+
return strings.startupIssueUnresolvable()
|
|
370
|
+
case 'dependency':
|
|
371
|
+
return strings.startupIssueDependency()
|
|
372
|
+
case 'duplicate-id':
|
|
373
|
+
return strings.startupIssueDuplicate()
|
|
374
|
+
default:
|
|
375
|
+
return type
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
329
379
|
function eventLabel(type) {
|
|
330
380
|
return (EVENT_LABELS[type] ?? (() => type))()
|
|
331
381
|
}
|
|
@@ -1007,6 +1057,62 @@ function EventList({ events }) {
|
|
|
1007
1057
|
)
|
|
1008
1058
|
}
|
|
1009
1059
|
|
|
1060
|
+
/** 启动区问题(issue #144):启动名册静态预检发现的问题条目,置顶展示
|
|
1061
|
+
* 修复命令与移除提示——名册中的坏条目是 all-or-nothing 启动失败的源头。 */
|
|
1062
|
+
function StartupIssuesBlock({ issues, checkedAt }) {
|
|
1063
|
+
if (!Array.isArray(issues) || issues.length === 0) return null
|
|
1064
|
+
return createElement(
|
|
1065
|
+
'div',
|
|
1066
|
+
{ className: 'dsh-my-guardian-startup-issues' },
|
|
1067
|
+
createElement(
|
|
1068
|
+
'div',
|
|
1069
|
+
{ className: 'dsh-my-guardian-startup-issues-title' },
|
|
1070
|
+
icon.alert(14),
|
|
1071
|
+
strings.startupIssues(),
|
|
1072
|
+
createElement('span', { className: 'dsh-my-guardian-startup-issues-count' }, String(issues.length)),
|
|
1073
|
+
typeof checkedAt === 'number' && Number.isFinite(checkedAt)
|
|
1074
|
+
? createElement('span', { className: 'dsh-my-guardian-startup-issues-time' }, formatTime(checkedAt))
|
|
1075
|
+
: null,
|
|
1076
|
+
),
|
|
1077
|
+
issues.map((issue, index) =>
|
|
1078
|
+
createElement(
|
|
1079
|
+
'div',
|
|
1080
|
+
{ className: 'dsh-my-guardian-startup-issue', key: index },
|
|
1081
|
+
createElement(
|
|
1082
|
+
'div',
|
|
1083
|
+
{ className: 'dsh-my-guardian-startup-issue-head' },
|
|
1084
|
+
createElement(
|
|
1085
|
+
'span',
|
|
1086
|
+
{
|
|
1087
|
+
className: `dsh-my-guardian-event-badge dsh-my-guardian-startup-issue-badge-${issue.type}`,
|
|
1088
|
+
title: issue.entryId,
|
|
1089
|
+
},
|
|
1090
|
+
startupIssueLabel(issue.type),
|
|
1091
|
+
),
|
|
1092
|
+
createElement('span', { className: 'dsh-my-guardian-startup-issue-name' }, issue.name),
|
|
1093
|
+
),
|
|
1094
|
+
createElement('div', { className: 'dsh-my-guardian-startup-issue-message' }, issue.message),
|
|
1095
|
+
typeof issue.fix === 'string' && issue.fix !== ''
|
|
1096
|
+
? createElement(
|
|
1097
|
+
'div',
|
|
1098
|
+
{ className: 'dsh-my-guardian-startup-issue-line' },
|
|
1099
|
+
createElement('span', { className: 'dsh-my-guardian-startup-issue-label' }, strings.startupIssueFix()),
|
|
1100
|
+
createElement('code', null, issue.fix),
|
|
1101
|
+
)
|
|
1102
|
+
: null,
|
|
1103
|
+
typeof issue.remove === 'string' && issue.remove !== ''
|
|
1104
|
+
? createElement(
|
|
1105
|
+
'div',
|
|
1106
|
+
{ className: 'dsh-my-guardian-startup-issue-line' },
|
|
1107
|
+
createElement('span', { className: 'dsh-my-guardian-startup-issue-label' }, strings.startupIssueRemove()),
|
|
1108
|
+
createElement('span', { className: 'dsh-my-guardian-startup-issue-remove' }, issue.remove),
|
|
1109
|
+
)
|
|
1110
|
+
: null,
|
|
1111
|
+
),
|
|
1112
|
+
),
|
|
1113
|
+
)
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1010
1116
|
function GuardianView({ visible }) {
|
|
1011
1117
|
const { state, loadFailed, reload, onAction, onSafeMode } = useGuardianState(visible)
|
|
1012
1118
|
|
|
@@ -1028,6 +1134,10 @@ function GuardianView({ visible }) {
|
|
|
1028
1134
|
'div',
|
|
1029
1135
|
{ className: 'dsh-my-guardian-root' },
|
|
1030
1136
|
createElement(SafeModeBar, { safeMode: state.safeMode, onSafeMode }),
|
|
1137
|
+
createElement(StartupIssuesBlock, {
|
|
1138
|
+
issues: state.startupIssues,
|
|
1139
|
+
checkedAt: state.startupCheckedAt,
|
|
1140
|
+
}),
|
|
1031
1141
|
loadFailed
|
|
1032
1142
|
? createElement(
|
|
1033
1143
|
'div',
|
package/lib/dep-precheck.js
CHANGED
|
@@ -14,7 +14,9 @@ import { satisfies } from './dep-version.js'
|
|
|
14
14
|
|
|
15
15
|
// Locate a package directory below a node_modules root, following symlinks
|
|
16
16
|
// (pnpm store / npm link both expose package.json through the mirrored dir).
|
|
17
|
-
|
|
17
|
+
// Exported for the startup-roster pre-check (issue #144): resolvability of a
|
|
18
|
+
// roster plugin's own package is verified before peer dependencies.
|
|
19
|
+
export function findModuleDir(nmRoot, packageName) {
|
|
18
20
|
const dir = join(nmRoot, packageName)
|
|
19
21
|
return existsSync(join(dir, 'package.json')) ? dir : null
|
|
20
22
|
}
|
package/lib/index.js
CHANGED
|
@@ -37,6 +37,7 @@ import { createPersister, createState } from './state.js'
|
|
|
37
37
|
import { findRootTree, createMountOps, initialScan } from './mount.js'
|
|
38
38
|
import { createApi } from './api.js'
|
|
39
39
|
import { attachEventListeners, logEvent } from './events.js'
|
|
40
|
+
import { runStartupCheck } from './startup-check.js'
|
|
40
41
|
|
|
41
42
|
export const name = 'dsh-my-guardian'
|
|
42
43
|
|
|
@@ -69,6 +70,7 @@ function applyInner(ctx) {
|
|
|
69
70
|
scheduleInitialScan(ctx, shared)
|
|
70
71
|
startWatchers(ctx, shared)
|
|
71
72
|
registerTeardown(ctx, shared)
|
|
73
|
+
registerStatusQuery(ctx, shared)
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
/** Mutable per-instance runtime context shared by every sub-module. */
|
|
@@ -84,6 +86,9 @@ function createShared({ tree, profileDir }) {
|
|
|
84
86
|
tree,
|
|
85
87
|
profileDir,
|
|
86
88
|
stagedFile: join(profileDir, 'cordis.staged.json'),
|
|
89
|
+
// startup-roster pre-check (issue #144): filled by runStartupCheck
|
|
90
|
+
startupIssues: [],
|
|
91
|
+
startupCheckedAt: null,
|
|
87
92
|
}
|
|
88
93
|
}
|
|
89
94
|
|
|
@@ -99,7 +104,14 @@ function wireServices(ctx, shared) {
|
|
|
99
104
|
// ── run after boot settles ──────────────────────────────────────────────
|
|
100
105
|
function scheduleInitialScan(ctx, shared) {
|
|
101
106
|
void Promise.resolve().then(() => {
|
|
102
|
-
|
|
107
|
+
// Startup-roster static pre-check (issue #144): best-effort, MUST never
|
|
108
|
+
// block or break the boot — a pre-check failure only records a report.
|
|
109
|
+
void runStartupCheck(ctx, shared).catch((error) => {
|
|
110
|
+
ctx.logger?.warn(
|
|
111
|
+
`[dsh-my-guardian] startup pre-check failed (recorded only): ${error instanceof Error ? error.message : String(error)}`,
|
|
112
|
+
)
|
|
113
|
+
})
|
|
114
|
+
void initialScan(shared).catch((error) => {
|
|
103
115
|
// the scan must never take the process down
|
|
104
116
|
ctx.logger?.warn(
|
|
105
117
|
`[dsh-my-guardian] initial scan failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -126,6 +138,28 @@ function startWatchers(ctx, shared) {
|
|
|
126
138
|
}, POLL_MS)
|
|
127
139
|
}
|
|
128
140
|
|
|
141
|
+
/** 插件状态查询(#155 聚合层):返回 guardian 加载状态 + 最近事件。 */
|
|
142
|
+
function registerStatusQuery(ctx, shared) {
|
|
143
|
+
ctx.on('plugin:status-query', ({ plugin }) => {
|
|
144
|
+
if (plugin !== 'dsh-my-guardian') return undefined
|
|
145
|
+
const lastActions = (shared.state.events ?? []).slice(-5).map((e) => ({
|
|
146
|
+
time: e.time ?? e.timestamp ?? Date.now(),
|
|
147
|
+
type: e.type ?? 'event',
|
|
148
|
+
detail: e.message ?? e.detail ?? '',
|
|
149
|
+
}))
|
|
150
|
+
return {
|
|
151
|
+
ok: true,
|
|
152
|
+
value: {
|
|
153
|
+
plugin: 'dsh-my-guardian',
|
|
154
|
+
config: { keys: ['safeMode', 'stagedFile'] },
|
|
155
|
+
running: shared.ready,
|
|
156
|
+
stats: { mounted: shared.mounted.size, safeMode: shared.state.safeMode === true },
|
|
157
|
+
lastActions,
|
|
158
|
+
},
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
|
|
129
163
|
/** teardown: unmount everything the guardian mounted, then persist. */
|
|
130
164
|
function registerTeardown(ctx, shared) {
|
|
131
165
|
ctx.effect(
|
package/lib/parts/styles.part.js
CHANGED
|
@@ -148,6 +148,33 @@ const STYLES = `
|
|
|
148
148
|
.dsh-my-guardian-event-message { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
|
|
149
149
|
font:var(--dsw-font-xxs-12); color:var(--dsw-alias-label-secondary); }
|
|
150
150
|
.dsh-my-guardian-event-time { flex:none; font:var(--dsw-font-xxxs-11); color:var(--dsw-alias-label-tertiary); white-space:nowrap; }
|
|
151
|
+
/* ── startup-roster issues (issue #144): pinned block above the list ────── */
|
|
152
|
+
.dsh-my-guardian-startup-issues { display:flex; flex-direction:column; gap:2px; margin-top:4px; padding:6px 8px; border-radius:8px;
|
|
153
|
+
border:1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 45%, transparent);
|
|
154
|
+
background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 6%, transparent);
|
|
155
|
+
animation:dsh-my-guardian-row-in 150ms var(--ds-ease-in-out); }
|
|
156
|
+
.dsh-my-guardian-startup-issues-title { display:flex; align-items:center; gap:5px; padding:0 2px 3px;
|
|
157
|
+
font:var(--dsw-font-xxs-strong-12); color:var(--dsw-alias-state-error-primary); }
|
|
158
|
+
.dsh-my-guardian-startup-issues-title svg { display:block; flex:none; }
|
|
159
|
+
.dsh-my-guardian-startup-issues-count { display:inline-flex; align-items:center; justify-content:center; min-width:17px; height:17px; padding:0 5px; border-radius:4px;
|
|
160
|
+
font:var(--dsw-font-xxxs-strong-11); color:var(--dsw-alias-label-primary-foreground); background:var(--dsw-alias-state-error-primary); }
|
|
161
|
+
.dsh-my-guardian-startup-issues-time { flex:1; text-align:right; font:var(--dsw-font-xxxs-11); color:var(--dsw-alias-label-tertiary); }
|
|
162
|
+
.dsh-my-guardian-startup-issue { display:flex; flex-direction:column; gap:2px; padding:5px 6px; border-radius:6px;
|
|
163
|
+
border:1px solid var(--dsw-alias-border-l1); background:transparent; }
|
|
164
|
+
.dsh-my-guardian-startup-issue:hover { background:var(--dsw-alias-interactive-bg-hover); }
|
|
165
|
+
.dsh-my-guardian-startup-issue-head { display:flex; align-items:center; gap:6px; min-width:0; }
|
|
166
|
+
.dsh-my-guardian-startup-issue-name { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
|
|
167
|
+
font:var(--dsw-font-s-strong-14); color:var(--dsw-alias-label-primary); }
|
|
168
|
+
.dsh-my-guardian-startup-issue-badge-unresolvable { color:var(--dsw-alias-state-error-primary); background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 14%, transparent); }
|
|
169
|
+
.dsh-my-guardian-startup-issue-badge-duplicate-id { color:var(--dsw-alias-state-error-primary); background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 14%, transparent); }
|
|
170
|
+
.dsh-my-guardian-startup-issue-badge-dependency { color:var(--dsw-alias-state-warn-primary); background:color-mix(in srgb, var(--dsw-alias-state-warn-primary) 16%, transparent); }
|
|
171
|
+
.dsh-my-guardian-startup-issue-message { font:var(--dsw-font-xxs-12); color:var(--dsw-alias-label-secondary); line-height:1.6; }
|
|
172
|
+
.dsh-my-guardian-startup-issue-line { display:flex; align-items:flex-start; gap:5px; padding:2px 0;
|
|
173
|
+
font:var(--dsw-font-xxxs-11); color:var(--dsw-alias-label-tertiary); line-height:1.6; min-width:0; }
|
|
174
|
+
.dsh-my-guardian-startup-issue-label { flex:none; color:var(--dsw-alias-label-tertiary); }
|
|
175
|
+
.dsh-my-guardian-startup-issue-line code { font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size:var(--dsw-font-xxxs-11);
|
|
176
|
+
color:var(--dsw-alias-state-success-primary); word-break:break-all; }
|
|
177
|
+
.dsh-my-guardian-startup-issue-remove { min-width:0; word-break:break-all; }
|
|
151
178
|
@keyframes dsh-my-guardian-row-in { from { opacity:0; transform:translateY(1px); } to { opacity:1; transform:none; } }
|
|
152
179
|
@keyframes dsh-my-guardian-spin { to { transform:rotate(360deg); } }
|
|
153
180
|
`
|
package/lib/parts/util.part.js
CHANGED
|
@@ -49,6 +49,13 @@ const strings = {
|
|
|
49
49
|
failureCode: () => (isZh() ? '代码错误' : 'Code error'),
|
|
50
50
|
failureOther: () => (isZh() ? '其他' : 'Other'),
|
|
51
51
|
installHint: () => (isZh() ? '安装建议' : 'Install'),
|
|
52
|
+
// ── startup-roster issues (issue #144) ─────────────────────────────────
|
|
53
|
+
startupIssues: () => (isZh() ? '启动区问题' : 'Startup roster issues'),
|
|
54
|
+
startupIssueFix: () => (isZh() ? '修复' : 'Fix'),
|
|
55
|
+
startupIssueRemove: () => (isZh() ? '移除' : 'Remove'),
|
|
56
|
+
startupIssueUnresolvable: () => (isZh() ? '包不可解析' : 'Unresolvable'),
|
|
57
|
+
startupIssueDependency: () => (isZh() ? '依赖缺失' : 'Dependency'),
|
|
58
|
+
startupIssueDuplicate: () => (isZh() ? '重复 id' : 'Duplicate id'),
|
|
52
59
|
}
|
|
53
60
|
|
|
54
61
|
// ── api ───────────────────────────────────────────────────────────────
|
|
@@ -116,6 +123,7 @@ const EVENT_LABELS = {
|
|
|
116
123
|
safe: () => (isZh() ? '安全模式' : 'Safe mode'),
|
|
117
124
|
'safe-mode': () => (isZh() ? '安全模式' : 'Safe mode'),
|
|
118
125
|
skip: () => (isZh() ? '跳过' : 'Skipped'),
|
|
126
|
+
'startup-issue': () => (isZh() ? '启动区问题' : 'Startup issue'),
|
|
119
127
|
}
|
|
120
128
|
|
|
121
129
|
/** Badge color variant for an event type; unknown types fall back to the
|
|
@@ -128,6 +136,7 @@ function eventVariant(type) {
|
|
|
128
136
|
return 'accent'
|
|
129
137
|
case 'quarantine':
|
|
130
138
|
case 'update-failed':
|
|
139
|
+
case 'startup-issue':
|
|
131
140
|
return 'danger'
|
|
132
141
|
case 'freeze':
|
|
133
142
|
case 'safe':
|
|
@@ -138,6 +147,20 @@ function eventVariant(type) {
|
|
|
138
147
|
}
|
|
139
148
|
}
|
|
140
149
|
|
|
150
|
+
/** Startup-issue badge label (issue #144): unresolvable / dependency / dup. */
|
|
151
|
+
function startupIssueLabel(type) {
|
|
152
|
+
switch (type) {
|
|
153
|
+
case 'unresolvable':
|
|
154
|
+
return strings.startupIssueUnresolvable()
|
|
155
|
+
case 'dependency':
|
|
156
|
+
return strings.startupIssueDependency()
|
|
157
|
+
case 'duplicate-id':
|
|
158
|
+
return strings.startupIssueDuplicate()
|
|
159
|
+
default:
|
|
160
|
+
return type
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
141
164
|
function eventLabel(type) {
|
|
142
165
|
return (EVENT_LABELS[type] ?? (() => type))()
|
|
143
166
|
}
|
package/lib/parts/view.part.js
CHANGED
|
@@ -155,6 +155,62 @@ function EventList({ events }) {
|
|
|
155
155
|
)
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
/** 启动区问题(issue #144):启动名册静态预检发现的问题条目,置顶展示
|
|
159
|
+
* 修复命令与移除提示——名册中的坏条目是 all-or-nothing 启动失败的源头。 */
|
|
160
|
+
function StartupIssuesBlock({ issues, checkedAt }) {
|
|
161
|
+
if (!Array.isArray(issues) || issues.length === 0) return null
|
|
162
|
+
return createElement(
|
|
163
|
+
'div',
|
|
164
|
+
{ className: 'dsh-my-guardian-startup-issues' },
|
|
165
|
+
createElement(
|
|
166
|
+
'div',
|
|
167
|
+
{ className: 'dsh-my-guardian-startup-issues-title' },
|
|
168
|
+
icon.alert(14),
|
|
169
|
+
strings.startupIssues(),
|
|
170
|
+
createElement('span', { className: 'dsh-my-guardian-startup-issues-count' }, String(issues.length)),
|
|
171
|
+
typeof checkedAt === 'number' && Number.isFinite(checkedAt)
|
|
172
|
+
? createElement('span', { className: 'dsh-my-guardian-startup-issues-time' }, formatTime(checkedAt))
|
|
173
|
+
: null,
|
|
174
|
+
),
|
|
175
|
+
issues.map((issue, index) =>
|
|
176
|
+
createElement(
|
|
177
|
+
'div',
|
|
178
|
+
{ className: 'dsh-my-guardian-startup-issue', key: index },
|
|
179
|
+
createElement(
|
|
180
|
+
'div',
|
|
181
|
+
{ className: 'dsh-my-guardian-startup-issue-head' },
|
|
182
|
+
createElement(
|
|
183
|
+
'span',
|
|
184
|
+
{
|
|
185
|
+
className: `dsh-my-guardian-event-badge dsh-my-guardian-startup-issue-badge-${issue.type}`,
|
|
186
|
+
title: issue.entryId,
|
|
187
|
+
},
|
|
188
|
+
startupIssueLabel(issue.type),
|
|
189
|
+
),
|
|
190
|
+
createElement('span', { className: 'dsh-my-guardian-startup-issue-name' }, issue.name),
|
|
191
|
+
),
|
|
192
|
+
createElement('div', { className: 'dsh-my-guardian-startup-issue-message' }, issue.message),
|
|
193
|
+
typeof issue.fix === 'string' && issue.fix !== ''
|
|
194
|
+
? createElement(
|
|
195
|
+
'div',
|
|
196
|
+
{ className: 'dsh-my-guardian-startup-issue-line' },
|
|
197
|
+
createElement('span', { className: 'dsh-my-guardian-startup-issue-label' }, strings.startupIssueFix()),
|
|
198
|
+
createElement('code', null, issue.fix),
|
|
199
|
+
)
|
|
200
|
+
: null,
|
|
201
|
+
typeof issue.remove === 'string' && issue.remove !== ''
|
|
202
|
+
? createElement(
|
|
203
|
+
'div',
|
|
204
|
+
{ className: 'dsh-my-guardian-startup-issue-line' },
|
|
205
|
+
createElement('span', { className: 'dsh-my-guardian-startup-issue-label' }, strings.startupIssueRemove()),
|
|
206
|
+
createElement('span', { className: 'dsh-my-guardian-startup-issue-remove' }, issue.remove),
|
|
207
|
+
)
|
|
208
|
+
: null,
|
|
209
|
+
),
|
|
210
|
+
),
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
158
214
|
function GuardianView({ visible }) {
|
|
159
215
|
const { state, loadFailed, reload, onAction, onSafeMode } = useGuardianState(visible)
|
|
160
216
|
|
|
@@ -176,6 +232,10 @@ function GuardianView({ visible }) {
|
|
|
176
232
|
'div',
|
|
177
233
|
{ className: 'dsh-my-guardian-root' },
|
|
178
234
|
createElement(SafeModeBar, { safeMode: state.safeMode, onSafeMode }),
|
|
235
|
+
createElement(StartupIssuesBlock, {
|
|
236
|
+
issues: state.startupIssues,
|
|
237
|
+
checkedAt: state.startupCheckedAt,
|
|
238
|
+
}),
|
|
179
239
|
loadFailed
|
|
180
240
|
? createElement(
|
|
181
241
|
'div',
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-my-guardian — startup-roster static pre-check (issue #144).
|
|
3
|
+
*
|
|
4
|
+
* DSH boots its plugin roster all-or-nothing: any roster row (cordis.patch.yml /
|
|
5
|
+
* profile / bundles — everything merged into the loader's include tree) that
|
|
6
|
+
* fails to import, throws during apply, or stays pending takes the whole
|
|
7
|
+
* `dsh web` process down, and the guardian (itself a roster row) has no
|
|
8
|
+
* chance to quarantine anything after the fact. This module closes that gap
|
|
9
|
+
* with a STATIC, best-effort pre-flight that runs right after the guardian's
|
|
10
|
+
* own apply:
|
|
11
|
+
*
|
|
12
|
+
* - package resolvability: every dsh-* roster entry's package must exist
|
|
13
|
+
* under the profile node_modules — an unresolvable package always fails
|
|
14
|
+
* the import stage (assertEntriesLoaded throws in dsh-app-boot)
|
|
15
|
+
* - peer dependencies: reuse the staged-mount pre-check (dep-precheck.js)
|
|
16
|
+
* — missing/out-of-range peers surface at boot as module-resolution
|
|
17
|
+
* failures, so they are flagged before the next restart hits them
|
|
18
|
+
* - duplicate entry ids: EntryGroup.update in cordis-plugin-loader throws
|
|
19
|
+
* "duplicate loader entry id" when a group config lists the same id
|
|
20
|
+
* twice — a guaranteed boot failure worth flagging up front
|
|
21
|
+
*
|
|
22
|
+
* The report is written to $DSH_HOME/guardian/startup-issues.json so it
|
|
23
|
+
* survives a broken boot: applies run in parallel with roster imports, so the
|
|
24
|
+
* guardian can usually record the report before assertEntriesActivated
|
|
25
|
+
* rejects the boot.
|
|
26
|
+
*
|
|
27
|
+
* Guardrails (watchdog self-protection, R15): the pre-check itself NEVER
|
|
28
|
+
* fails the boot — every step is wrapped, failures only log; a broken loader
|
|
29
|
+
* tree simply yields no roster and an empty report.
|
|
30
|
+
*/
|
|
31
|
+
import { join } from 'node:path'
|
|
32
|
+
import { findModuleDir, checkPeerDependencies, buildDependencyMessage } from './dep-precheck.js'
|
|
33
|
+
import { writeStartupIssuesFile } from './state.js'
|
|
34
|
+
import { logEvent } from './events.js'
|
|
35
|
+
|
|
36
|
+
/** Profile-node_modules root used for roster resolvability checks. */
|
|
37
|
+
function profileNodeModules(profileDir) {
|
|
38
|
+
return join(profileDir, 'node_modules')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Collect every entry of a tree, tolerantly: entries() when present (real
|
|
42
|
+
* IncludeTree includes nested subtrees), otherwise a plain store walk
|
|
43
|
+
* (mocked trees in tests). Never throws — a broken tree yields []. */
|
|
44
|
+
export function collectTreeEntries(tree) {
|
|
45
|
+
if (tree === null || typeof tree !== 'object') return []
|
|
46
|
+
if (typeof tree.entries === 'function') {
|
|
47
|
+
try {
|
|
48
|
+
const out = []
|
|
49
|
+
for (const entry of tree.entries()) {
|
|
50
|
+
if (entry !== null && typeof entry === 'object') out.push(entry)
|
|
51
|
+
}
|
|
52
|
+
return out
|
|
53
|
+
} catch {
|
|
54
|
+
// fall through to the store walk
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const store = tree.store
|
|
58
|
+
if (store === null || typeof store !== 'object') return []
|
|
59
|
+
return Object.values(store).filter((entry) => entry !== null && typeof entry === 'object')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Read one string field from an entry's options (tolerant of odd shapes). */
|
|
63
|
+
function readOptionString(options, key) {
|
|
64
|
+
return options !== null && typeof options === 'object' && typeof options[key] === 'string' && options[key] !== ''
|
|
65
|
+
? options[key]
|
|
66
|
+
: ''
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Extract { id, name } from an entry, with safe fallbacks (entry.id getter
|
|
70
|
+
* may throw on partially-constructed entries — never let it break the boot). */
|
|
71
|
+
export function entryLabels(entry) {
|
|
72
|
+
const options = entry !== null && typeof entry === 'object' ? entry.options : null
|
|
73
|
+
let id = readOptionString(options, 'id')
|
|
74
|
+
if (id === '') id = entryIdGetterOf(entry)
|
|
75
|
+
const name = readOptionString(options, 'name')
|
|
76
|
+
return { id, name: name !== '' ? name : id !== '' ? id : '?' }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** entry.id getter fallback (wrapped: it may throw on partial entries). */
|
|
80
|
+
function entryIdGetterOf(entry) {
|
|
81
|
+
try {
|
|
82
|
+
const entryId = entry?.id
|
|
83
|
+
return typeof entryId === 'string' && entryId !== '' ? entryId : ''
|
|
84
|
+
} catch {
|
|
85
|
+
return ''
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** True when an entry is disabled (explicit row or via options) — disabled
|
|
90
|
+
* rows are exempt from the boot audits and from this pre-check. */
|
|
91
|
+
export function isDisabledEntry(entry) {
|
|
92
|
+
if (entry === null || typeof entry !== 'object') return false
|
|
93
|
+
if (entry.disabled === true) return true
|
|
94
|
+
const options = entry.options
|
|
95
|
+
return options !== null && typeof options === 'object' && options.disabled === true
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Removal hint shared by every issue kind (guardian never rewrites YAML). */
|
|
99
|
+
const REMOVE_HINT = '从启动名册(cordis.patch.yml / profile)中删除该条目行,或标记 disabled: true 暂缓加载'
|
|
100
|
+
|
|
101
|
+
/** Build an unresolvable-package issue (import stage would fail). */
|
|
102
|
+
function unresolvedIssue(id, name) {
|
|
103
|
+
return {
|
|
104
|
+
type: 'unresolvable',
|
|
105
|
+
entryId: id,
|
|
106
|
+
name,
|
|
107
|
+
message: `插件包 ${name} 无法解析(profile node_modules 中不存在),启动 import 将失败`,
|
|
108
|
+
fix: `dsh plugin add ${name}`,
|
|
109
|
+
remove: REMOVE_HINT,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Build a dependency issue reusing the staged-mount pre-check result. */
|
|
114
|
+
function dependencyIssue(id, name, precheck) {
|
|
115
|
+
return {
|
|
116
|
+
type: 'dependency',
|
|
117
|
+
entryId: id,
|
|
118
|
+
name,
|
|
119
|
+
message: buildDependencyMessage(precheck),
|
|
120
|
+
missingDeps: [...precheck.missing, ...precheck.mismatched.map((item) => item.name)],
|
|
121
|
+
installHint: precheck.suggestions[0] ?? null,
|
|
122
|
+
fix: precheck.suggestions[0] ?? `dsh plugin add ${name}`,
|
|
123
|
+
remove: REMOVE_HINT,
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Build a duplicate-id issue for one id seen more than once. */
|
|
128
|
+
function duplicateIssue(id, names) {
|
|
129
|
+
return {
|
|
130
|
+
type: 'duplicate-id',
|
|
131
|
+
entryId: id,
|
|
132
|
+
name: names[0] ?? id,
|
|
133
|
+
message: `名册存在重复条目 id "${id}"(${names.length} 处:${names.join('、')}),加载时将抛 duplicate loader entry id`,
|
|
134
|
+
fix: null,
|
|
135
|
+
remove: '从名册中删除重复的条目行(每个 id 保留一条)',
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Record an entry under its id for the duplicate-id sweep. */
|
|
140
|
+
function addSeen(map, id, item) {
|
|
141
|
+
const group = map.get(id)
|
|
142
|
+
if (group === undefined) map.set(id, [item])
|
|
143
|
+
else group.push(item)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Static checks for one roster row: resolvability + peers (dsh-* only). */
|
|
147
|
+
function checkPluginItem(issues, item, nmRoot, profileDir) {
|
|
148
|
+
const id = typeof item.id === 'string' ? item.id : ''
|
|
149
|
+
const name = typeof item.name === 'string' ? item.name : ''
|
|
150
|
+
const label = name !== '' ? name : id
|
|
151
|
+
if (label === '' || !label.startsWith('dsh-')) return
|
|
152
|
+
const pluginDir = findModuleDir(nmRoot, label)
|
|
153
|
+
if (pluginDir === null) {
|
|
154
|
+
issues.push(unresolvedIssue(id, label))
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
const precheck = checkPeerDependencies({ profileDir, pluginName: label })
|
|
158
|
+
if (!precheck.ok) issues.push(dependencyIssue(id, label, precheck))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Append one duplicate-id issue per id seen in more than one roster row. */
|
|
162
|
+
function collectDuplicateIssues(issues, byId) {
|
|
163
|
+
for (const [id, group] of byId) {
|
|
164
|
+
if (group.length < 2) continue
|
|
165
|
+
issues.push(
|
|
166
|
+
duplicateIssue(
|
|
167
|
+
id,
|
|
168
|
+
group.map((item) => item.name ?? item.id ?? '?'),
|
|
169
|
+
),
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Core static pre-check over one roster's normalized entries.
|
|
176
|
+
* entries: [{ id, name, disabled }] (see normalizeRoster). Checks dsh-*
|
|
177
|
+
* plugins for package resolvability + peer deps, and the whole roster for
|
|
178
|
+
* duplicate ids. Never throws — returns { issues }.
|
|
179
|
+
*/
|
|
180
|
+
export function checkStartupRoster({ entries, profileDir }) {
|
|
181
|
+
const issues = []
|
|
182
|
+
const roster = Array.isArray(entries) ? entries : []
|
|
183
|
+
const nmRoot = profileNodeModules(profileDir)
|
|
184
|
+
const byId = new Map()
|
|
185
|
+
for (const item of roster) {
|
|
186
|
+
if (item === null || typeof item !== 'object') continue
|
|
187
|
+
const id = typeof item.id === 'string' ? item.id : ''
|
|
188
|
+
if (id !== '') addSeen(byId, id, item)
|
|
189
|
+
checkPluginItem(issues, item, nmRoot, profileDir)
|
|
190
|
+
}
|
|
191
|
+
collectDuplicateIssues(issues, byId)
|
|
192
|
+
return { issues }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Normalize raw tree entries into { id, name, disabled } roster rows. */
|
|
196
|
+
export function normalizeRoster(entries) {
|
|
197
|
+
return entries.map((entry) => {
|
|
198
|
+
const labels = entryLabels(entry)
|
|
199
|
+
return { id: labels.id, name: labels.name, disabled: isDisabledEntry(entry) }
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Full startup pre-flight for one guardian instance: collect the roster from
|
|
205
|
+
* the include tree, statically check it, persist the report and log an event.
|
|
206
|
+
* Never throws — every failure degrades to a warn log (boot must continue).
|
|
207
|
+
*/
|
|
208
|
+
export async function runStartupCheck(ctx, shared) {
|
|
209
|
+
const entries = collectTreeEntries(shared.tree)
|
|
210
|
+
const roster = normalizeRoster(entries).filter((item) => !item.disabled)
|
|
211
|
+
const { issues } = checkStartupRoster({ entries: roster, profileDir: shared.profileDir })
|
|
212
|
+
const checkedAt = Date.now()
|
|
213
|
+
await writeStartupIssuesFile({ version: 1, checkedAt, profileDir: shared.profileDir, issues })
|
|
214
|
+
shared.startupIssues = issues
|
|
215
|
+
shared.startupCheckedAt = checkedAt
|
|
216
|
+
if (issues.length > 0) {
|
|
217
|
+
const names = [...new Set(issues.map((issue) => issue.name))].slice(0, 3).join('、')
|
|
218
|
+
logEvent(
|
|
219
|
+
shared,
|
|
220
|
+
'startup-issue',
|
|
221
|
+
`启动名册静态预检发现 ${issues.length} 个问题(${names}…)— 详见 guardian/startup-issues.json`,
|
|
222
|
+
)
|
|
223
|
+
shared.persistSoon()
|
|
224
|
+
ctx.logger?.warn(
|
|
225
|
+
`[dsh-my-guardian] startup roster pre-check: ${issues.length} issue(s) — see $DSH_HOME/guardian/startup-issues.json`,
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
}
|
package/lib/state.js
CHANGED
|
@@ -32,6 +32,26 @@ function guardianDir() {
|
|
|
32
32
|
return join(homedir(), '.dsh', 'guardian')
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Persist the startup-roster pre-check report (issue #144) atomically at
|
|
37
|
+
* $DSH_HOME/guardian/startup-issues.json. The report is written even when
|
|
38
|
+
* the roster is healthy (empty issues + checkedAt) so the file doubles as a
|
|
39
|
+
* "last checked" marker; missing/corrupt file only means "never written".
|
|
40
|
+
* Never throws to callers — the guardian must not take the process down.
|
|
41
|
+
*/
|
|
42
|
+
export async function writeStartupIssuesFile(payload) {
|
|
43
|
+
const dir = guardianDir()
|
|
44
|
+
const file = join(dir, 'startup-issues.json')
|
|
45
|
+
const tmp = `${file}.tmp-${uniqueSuffix()}`
|
|
46
|
+
try {
|
|
47
|
+
await mkdir(dir, { recursive: true })
|
|
48
|
+
await writeFile(tmp, JSON.stringify(payload), 'utf8')
|
|
49
|
+
await rename(tmp, file)
|
|
50
|
+
} catch {
|
|
51
|
+
// the report is diagnostic-only: a write failure must not break boot
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
35
55
|
/** Empty state document. */
|
|
36
56
|
export function createState() {
|
|
37
57
|
return { version: 1, safeMode: false, staged: {}, promoted: {}, events: [] }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-my-guardian",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "DSH 插件治理守护:新装/更新插件先进候选区,启动后热挂载——成功转正、失败自动禁用、连续失败冻结,一键安全模式,侧边栏诊断面板。DSH web plugin: staged plugin loading with auto-disable on failure, freeze, safe mode and a sidebar panel.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -36,10 +36,7 @@
|
|
|
36
36
|
"patch": "./cordis.patch.yml"
|
|
37
37
|
},
|
|
38
38
|
"client": {
|
|
39
|
-
"platform": "web"
|
|
40
|
-
"inject": [
|
|
41
|
-
"@deepseek-ai/dsh-client-runtime"
|
|
42
|
-
]
|
|
39
|
+
"platform": "web"
|
|
43
40
|
}
|
|
44
41
|
},
|
|
45
42
|
"peerDependencies": {
|