dsh-config-manager 0.1.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.
Files changed (253) hide show
  1. package/README.md +216 -0
  2. package/README.zh-CN.md +218 -0
  3. package/cordis.patch.yml +45 -0
  4. package/dsh.bundle.patch +21 -0
  5. package/dsh.client +39 -0
  6. package/lib/adapters/agent-presets.d.ts +13 -0
  7. package/lib/adapters/agent-presets.js +14 -0
  8. package/lib/adapters/agent-presets.js.map +1 -0
  9. package/lib/adapters/credentials.d.ts +36 -0
  10. package/lib/adapters/credentials.js +121 -0
  11. package/lib/adapters/credentials.js.map +1 -0
  12. package/lib/adapters/file-collection.d.ts +14 -0
  13. package/lib/adapters/file-collection.js +102 -0
  14. package/lib/adapters/file-collection.js.map +1 -0
  15. package/lib/adapters/index.d.ts +38 -0
  16. package/lib/adapters/index.js +46 -0
  17. package/lib/adapters/index.js.map +1 -0
  18. package/lib/adapters/mcp.d.ts +27 -0
  19. package/lib/adapters/mcp.js +175 -0
  20. package/lib/adapters/mcp.js.map +1 -0
  21. package/lib/adapters/plugin-files.d.ts +15 -0
  22. package/lib/adapters/plugin-files.js +91 -0
  23. package/lib/adapters/plugin-files.js.map +1 -0
  24. package/lib/adapters/plugins.d.ts +13 -0
  25. package/lib/adapters/plugins.js +177 -0
  26. package/lib/adapters/plugins.js.map +1 -0
  27. package/lib/adapters/prompts.d.ts +29 -0
  28. package/lib/adapters/prompts.js +228 -0
  29. package/lib/adapters/prompts.js.map +1 -0
  30. package/lib/adapters/providers.d.ts +33 -0
  31. package/lib/adapters/providers.js +143 -0
  32. package/lib/adapters/providers.js.map +1 -0
  33. package/lib/adapters/sessions.d.ts +14 -0
  34. package/lib/adapters/sessions.js +15 -0
  35. package/lib/adapters/sessions.js.map +1 -0
  36. package/lib/adapters/settings.d.ts +23 -0
  37. package/lib/adapters/settings.js +142 -0
  38. package/lib/adapters/settings.js.map +1 -0
  39. package/lib/adapters/skills.d.ts +13 -0
  40. package/lib/adapters/skills.js +14 -0
  41. package/lib/adapters/skills.js.map +1 -0
  42. package/lib/adapters/test-helpers.d.ts +99 -0
  43. package/lib/adapters/test-helpers.js +192 -0
  44. package/lib/adapters/test-helpers.js.map +1 -0
  45. package/lib/adapters/ui.d.ts +26 -0
  46. package/lib/adapters/ui.js +88 -0
  47. package/lib/adapters/ui.js.map +1 -0
  48. package/lib/adapters/workspaces.d.ts +12 -0
  49. package/lib/adapters/workspaces.js +92 -0
  50. package/lib/adapters/workspaces.js.map +1 -0
  51. package/lib/client.d.ts +377 -0
  52. package/lib/client.js +2598 -0
  53. package/lib/client.js.map +1 -0
  54. package/lib/core/analyzer.d.ts +35 -0
  55. package/lib/core/analyzer.js +563 -0
  56. package/lib/core/analyzer.js.map +1 -0
  57. package/lib/core/backup.d.ts +29 -0
  58. package/lib/core/backup.js +184 -0
  59. package/lib/core/backup.js.map +1 -0
  60. package/lib/core/exporter.d.ts +37 -0
  61. package/lib/core/exporter.js +222 -0
  62. package/lib/core/exporter.js.map +1 -0
  63. package/lib/core/importer.d.ts +42 -0
  64. package/lib/core/importer.js +25 -0
  65. package/lib/core/importer.js.map +1 -0
  66. package/lib/core/index.d.ts +12 -0
  67. package/lib/core/index.js +11 -0
  68. package/lib/core/index.js.map +1 -0
  69. package/lib/core/rollback.d.ts +9 -0
  70. package/lib/core/rollback.js +136 -0
  71. package/lib/core/rollback.js.map +1 -0
  72. package/lib/core/types.d.ts +331 -0
  73. package/lib/core/types.js +18 -0
  74. package/lib/core/types.js.map +1 -0
  75. package/lib/core/validator.d.ts +18 -0
  76. package/lib/core/validator.js +77 -0
  77. package/lib/core/validator.js.map +1 -0
  78. package/lib/index.d.ts +51 -0
  79. package/lib/index.js +875 -0
  80. package/lib/index.js.map +1 -0
  81. package/lib/migrations/index.d.ts +29 -0
  82. package/lib/migrations/index.js +56 -0
  83. package/lib/migrations/index.js.map +1 -0
  84. package/lib/migrations/v1-to-v2.d.ts +9 -0
  85. package/lib/migrations/v1-to-v2.js +10 -0
  86. package/lib/migrations/v1-to-v2.js.map +1 -0
  87. package/lib/profiles/index.d.ts +4 -0
  88. package/lib/profiles/index.js +5 -0
  89. package/lib/profiles/index.js.map +1 -0
  90. package/lib/profiles/profile-manager.d.ts +102 -0
  91. package/lib/profiles/profile-manager.js +522 -0
  92. package/lib/profiles/profile-manager.js.map +1 -0
  93. package/lib/schema/config.d.ts +28 -0
  94. package/lib/schema/config.js +136 -0
  95. package/lib/schema/config.js.map +1 -0
  96. package/lib/schema/manifest.d.ts +28 -0
  97. package/lib/schema/manifest.js +109 -0
  98. package/lib/schema/manifest.js.map +1 -0
  99. package/lib/schema/types.d.ts +169 -0
  100. package/lib/schema/types.js +8 -0
  101. package/lib/schema/types.js.map +1 -0
  102. package/lib/schema/versions.d.ts +27 -0
  103. package/lib/schema/versions.js +50 -0
  104. package/lib/schema/versions.js.map +1 -0
  105. package/lib/security/encryption.d.ts +41 -0
  106. package/lib/security/encryption.js +152 -0
  107. package/lib/security/encryption.js.map +1 -0
  108. package/lib/security/index.d.ts +16 -0
  109. package/lib/security/index.js +17 -0
  110. package/lib/security/index.js.map +1 -0
  111. package/lib/security/integrity.d.ts +30 -0
  112. package/lib/security/integrity.js +78 -0
  113. package/lib/security/integrity.js.map +1 -0
  114. package/lib/security/redaction.d.ts +26 -0
  115. package/lib/security/redaction.js +95 -0
  116. package/lib/security/redaction.js.map +1 -0
  117. package/lib/security/secret-scanner.d.ts +48 -0
  118. package/lib/security/secret-scanner.js +253 -0
  119. package/lib/security/secret-scanner.js.map +1 -0
  120. package/lib/security/zip-security.d.ts +27 -0
  121. package/lib/security/zip-security.js +154 -0
  122. package/lib/security/zip-security.js.map +1 -0
  123. package/lib/ui/conflict-view.d.ts +28 -0
  124. package/lib/ui/conflict-view.js +49 -0
  125. package/lib/ui/conflict-view.js.map +1 -0
  126. package/lib/ui/errors.d.ts +17 -0
  127. package/lib/ui/errors.js +76 -0
  128. package/lib/ui/errors.js.map +1 -0
  129. package/lib/ui/export-flow.d.ts +62 -0
  130. package/lib/ui/export-flow.js +79 -0
  131. package/lib/ui/export-flow.js.map +1 -0
  132. package/lib/ui/import-wizard.d.ts +67 -0
  133. package/lib/ui/import-wizard.js +169 -0
  134. package/lib/ui/import-wizard.js.map +1 -0
  135. package/lib/ui/path-mapping.d.ts +38 -0
  136. package/lib/ui/path-mapping.js +75 -0
  137. package/lib/ui/path-mapping.js.map +1 -0
  138. package/lib/ui/progress.d.ts +27 -0
  139. package/lib/ui/progress.js +76 -0
  140. package/lib/ui/progress.js.map +1 -0
  141. package/lib/ui/report.d.ts +21 -0
  142. package/lib/ui/report.js +146 -0
  143. package/lib/ui/report.js.map +1 -0
  144. package/lib/ui/test-helpers.d.ts +62 -0
  145. package/lib/ui/test-helpers.js +147 -0
  146. package/lib/ui/test-helpers.js.map +1 -0
  147. package/lib/ui/types.d.ts +129 -0
  148. package/lib/ui/types.js +21 -0
  149. package/lib/ui/types.js.map +1 -0
  150. package/lib/utils/hashing.d.ts +17 -0
  151. package/lib/utils/hashing.js +41 -0
  152. package/lib/utils/hashing.js.map +1 -0
  153. package/lib/utils/json.d.ts +24 -0
  154. package/lib/utils/json.js +76 -0
  155. package/lib/utils/json.js.map +1 -0
  156. package/lib/utils/logger.d.ts +39 -0
  157. package/lib/utils/logger.js +82 -0
  158. package/lib/utils/logger.js.map +1 -0
  159. package/lib/utils/paths.d.ts +31 -0
  160. package/lib/utils/paths.js +132 -0
  161. package/lib/utils/paths.js.map +1 -0
  162. package/lib/utils/zip.d.ts +54 -0
  163. package/lib/utils/zip.js +285 -0
  164. package/lib/utils/zip.js.map +1 -0
  165. package/package.json +109 -0
  166. package/src/adapters/agent-presets.ts +14 -0
  167. package/src/adapters/credentials.test.ts +90 -0
  168. package/src/adapters/credentials.ts +141 -0
  169. package/src/adapters/file-collection.ts +110 -0
  170. package/src/adapters/files.test.ts +113 -0
  171. package/src/adapters/index.ts +69 -0
  172. package/src/adapters/mcp.test.ts +96 -0
  173. package/src/adapters/mcp.ts +181 -0
  174. package/src/adapters/plugin-files.ts +96 -0
  175. package/src/adapters/plugins.test.ts +72 -0
  176. package/src/adapters/plugins.ts +176 -0
  177. package/src/adapters/prompts.test.ts +110 -0
  178. package/src/adapters/prompts.ts +226 -0
  179. package/src/adapters/providers.test.ts +125 -0
  180. package/src/adapters/providers.ts +161 -0
  181. package/src/adapters/roundtrip.test.ts +188 -0
  182. package/src/adapters/sessions.ts +15 -0
  183. package/src/adapters/settings.test.ts +93 -0
  184. package/src/adapters/settings.ts +165 -0
  185. package/src/adapters/skills.ts +14 -0
  186. package/src/adapters/test-helpers.ts +207 -0
  187. package/src/adapters/ui.test.ts +56 -0
  188. package/src/adapters/ui.ts +106 -0
  189. package/src/adapters/workspaces.test.ts +79 -0
  190. package/src/adapters/workspaces.ts +95 -0
  191. package/src/client/CLIENT_DEPENDENCIES.md +101 -0
  192. package/src/client/ConfigManagerSection.tsx +60 -0
  193. package/src/client/api.ts +270 -0
  194. package/src/client/client-types.ts +27 -0
  195. package/src/client/common/ErrorBanner.tsx +68 -0
  196. package/src/client/common/ProgressBar.tsx +49 -0
  197. package/src/client/common/ReportView.tsx +117 -0
  198. package/src/client/common/ui.tsx +165 -0
  199. package/src/client/config-manager.module.css +759 -0
  200. package/src/client/css-modules.d.ts +8 -0
  201. package/src/client/export/ExportView.tsx +222 -0
  202. package/src/client/import/ConflictList.tsx +72 -0
  203. package/src/client/import/ImportWizardView.tsx +406 -0
  204. package/src/client/import/PathMappingForm.tsx +77 -0
  205. package/src/client/index.ts +65 -0
  206. package/src/client/locales.ts +182 -0
  207. package/src/core/analyzer.ts +631 -0
  208. package/src/core/backup.ts +212 -0
  209. package/src/core/exporter.ts +246 -0
  210. package/src/core/importer.ts +66 -0
  211. package/src/core/index.ts +39 -0
  212. package/src/core/rollback.ts +140 -0
  213. package/src/core/smoke.test.ts +717 -0
  214. package/src/core/types.ts +343 -0
  215. package/src/core/validator.ts +84 -0
  216. package/src/index.ts +942 -0
  217. package/src/migrations/index.ts +71 -0
  218. package/src/migrations/v1-to-v2.ts +17 -0
  219. package/src/profiles/index.ts +8 -0
  220. package/src/profiles/profile-manager.test.ts +280 -0
  221. package/src/profiles/profile-manager.ts +606 -0
  222. package/src/schema/config.ts +137 -0
  223. package/src/schema/manifest.ts +124 -0
  224. package/src/schema/types.ts +179 -0
  225. package/src/schema/versions.ts +55 -0
  226. package/src/security/encryption.ts +180 -0
  227. package/src/security/index.ts +16 -0
  228. package/src/security/integrity.ts +102 -0
  229. package/src/security/redaction.ts +101 -0
  230. package/src/security/secret-scanner.ts +275 -0
  231. package/src/security/security.test.ts +796 -0
  232. package/src/security/zip-security.ts +180 -0
  233. package/src/ui/conflict-view.test.ts +62 -0
  234. package/src/ui/conflict-view.ts +65 -0
  235. package/src/ui/errors.test.ts +44 -0
  236. package/src/ui/errors.ts +95 -0
  237. package/src/ui/export-flow.test.ts +75 -0
  238. package/src/ui/export-flow.ts +128 -0
  239. package/src/ui/import-wizard.test.ts +128 -0
  240. package/src/ui/import-wizard.ts +208 -0
  241. package/src/ui/path-mapping.test.ts +67 -0
  242. package/src/ui/path-mapping.ts +103 -0
  243. package/src/ui/progress.test.ts +42 -0
  244. package/src/ui/progress.ts +90 -0
  245. package/src/ui/report.test.ts +93 -0
  246. package/src/ui/report.ts +140 -0
  247. package/src/ui/test-helpers.ts +177 -0
  248. package/src/ui/types.ts +193 -0
  249. package/src/utils/hashing.ts +53 -0
  250. package/src/utils/json.ts +77 -0
  251. package/src/utils/logger.ts +114 -0
  252. package/src/utils/paths.ts +122 -0
  253. package/src/utils/zip.ts +332 -0
@@ -0,0 +1,8 @@
1
+ /**
2
+ * CSS Modules 类型声明(dsh-ssh 同款:src/client/css-modules.d.ts)。
3
+ * p3 的 tsdown 构建需配置 CSS Modules 处理(dsh-ssh 用 lightningcss)。
4
+ */
5
+ declare module '*.module.css' {
6
+ const classes: Record<string, string>
7
+ export default classes
8
+ }
@@ -0,0 +1,222 @@
1
+ /**
2
+ * 导出视图(规范 §1 / §21,绑 src/ui/export-flow.ts 的 ExportFlow 控制器)。
3
+ *
4
+ * - Quick:一键导出推荐分区(ExportFlow.quickSelection());
5
+ * - Custom:按 §1 分组目录(EXPORT_GROUPS × DEFAULT_CATEGORIES)逐项勾选,
6
+ * ExportFlow.validateSelection() 给出设备相关分区警告;
7
+ * - Include secrets:可选加密备份(需设置加密密码,密码仅本次内存使用,
8
+ * 经 api.exportPassword 随请求体传给 Host 半,绝不落盘/入 manifest);
9
+ * - 进度:ExportFlow.run 发出 ProgressEvent → ProgressBar;
10
+ * - 结果:ReportView(export) + 下载按钮(File System Access API 流式落盘)。
11
+ */
12
+ import { useCallback, useState } from 'react'
13
+ import type { ChangeEvent } from 'react'
14
+ import { ExportFlow, DEFAULT_CATEGORIES } from '../../ui/export-flow.ts'
15
+ import { EXPORT_GROUPS } from '../../ui/types.ts'
16
+ import type { ExportRunResult } from '../../ui/export-flow.ts'
17
+ import type { ProgressEvent } from '../../ui/types.ts'
18
+ import type { SectionId } from '../../schema/types.ts'
19
+ import type { TranslateNS } from '../client-types.ts'
20
+ import type { ConfigManagerApi } from '../api.ts'
21
+ import { Badge, Banner, Button, Card, Checkbox, SectionTitle, Spinner } from '../common/ui.tsx'
22
+ import { ErrorBanner } from '../common/ErrorBanner.tsx'
23
+ import { ProgressBar } from '../common/ProgressBar.tsx'
24
+ import { ReportView } from '../common/ReportView.tsx'
25
+ import css from '../config-manager.module.css'
26
+
27
+ export interface ExportViewProps {
28
+ api: ConfigManagerApi
29
+ t: TranslateNS<'config-manager'>
30
+ }
31
+
32
+ type ExportMode = 'quick' | 'custom'
33
+
34
+ /** Custom 模式的初始勾选 = 推荐分区(可再调整) */
35
+ function defaultCustomSelection(): SectionId[] {
36
+ return DEFAULT_CATEGORIES.filter((c) => c.defaultIncluded).map((c) => c.id)
37
+ }
38
+
39
+ /**
40
+ * 导出主视图:Quick/Custom 切换 → 勾选/密码 → 执行 → 进度 → 报告 → 下载。
41
+ */
42
+ export function ExportView({ api, t }: ExportViewProps) {
43
+ const [mode, setMode] = useState<ExportMode>('quick')
44
+ const [selection, setSelection] = useState<SectionId[]>(defaultCustomSelection)
45
+ const [includeSecrets, setIncludeSecrets] = useState(false)
46
+ const [password, setPassword] = useState('')
47
+ const [passwordConfirm, setPasswordConfirm] = useState('')
48
+ const [running, setRunning] = useState(false)
49
+ const [progress, setProgress] = useState<ProgressEvent | null>(null)
50
+ const [result, setResult] = useState<ExportRunResult | null>(null)
51
+ const [error, setError] = useState<unknown>(null)
52
+ const [downloaded, setDownloaded] = useState(false)
53
+
54
+ // ExportFlow 控制器实例(port=api,onProgress 桥到 React state)
55
+ const [flow] = useState(() => new ExportFlow({
56
+ port: api,
57
+ onProgress: (event) => { setProgress(event) },
58
+ }))
59
+
60
+ const toggleSection = useCallback((id: SectionId, checked: boolean) => {
61
+ setSelection((prev) => {
62
+ const has = prev.includes(id)
63
+ if (checked && !has) return [...prev, id]
64
+ if (!checked && has) return prev.filter((s) => s !== id)
65
+ return prev
66
+ })
67
+ }, [])
68
+
69
+ const passwordInvalid =
70
+ includeSecrets && (password === '' || password !== passwordConfirm)
71
+
72
+ /** 执行导出(Quick 或 Custom) */
73
+ const runExport = async (): Promise<void> => {
74
+ if (passwordInvalid) return
75
+ setRunning(true)
76
+ setError(null)
77
+ setResult(null)
78
+ setDownloaded(false)
79
+ setProgress(null)
80
+ try {
81
+ // 加密密码随本次导出请求体传给 Host 半(仅内存)
82
+ api.exportPassword = includeSecrets ? password : null
83
+ const run = await flow.run(mode, selection, { includeSecrets })
84
+ setResult(run)
85
+ } catch (err) {
86
+ setError(err)
87
+ } finally {
88
+ setRunning(false)
89
+ setProgress(null)
90
+ }
91
+ }
92
+
93
+ /** 下载导出的 ZIP 到本机 */
94
+ const download = async (): Promise<void> => {
95
+ if (result === null) return
96
+ try {
97
+ setError(null)
98
+ const outcome = await api.download(result.zipPath)
99
+ setDownloaded(true)
100
+ void outcome // blob/streamed 由 api 处理;此处仅确认成功
101
+ } catch (err) {
102
+ setError(err)
103
+ }
104
+ }
105
+
106
+ // Custom 模式下的设备相关分区警告
107
+ const customWarnings = mode === 'custom' ? flow.validateSelection(selection).warnings : []
108
+
109
+ return (
110
+ <div className={css.viewBody}>
111
+ <SectionTitle title={t('view.export')} subtitle={t('section.description')} />
112
+
113
+ {/* 模式切换 */}
114
+ <div className={css.modeTabs} role="tablist">
115
+ <button type="button" role="tab" aria-selected={mode === 'quick'} data-active={mode === 'quick' ? '' : undefined} className={css.modeTab} onClick={() => { setMode('quick') }}>
116
+ {t('export.mode.quick')}
117
+ </button>
118
+ <button type="button" role="tab" aria-selected={mode === 'custom'} data-active={mode === 'custom' ? '' : undefined} className={css.modeTab} onClick={() => { setMode('custom') }}>
119
+ {t('export.mode.custom')}
120
+ </button>
121
+ </div>
122
+ <div className={css.modeHint}>
123
+ {mode === 'quick' ? t('export.mode.quickHint') : t('export.mode.customHint')}
124
+ </div>
125
+
126
+ {/* Custom:分组勾选目录 */}
127
+ {mode === 'custom' && (
128
+ <div className={css.groupList}>
129
+ {EXPORT_GROUPS.map((group) => {
130
+ const categories = flow.categories.filter((c) => c.group === group.id)
131
+ if (categories.length === 0) return null
132
+ return (
133
+ <Card key={group.id} className={css.groupCard}>
134
+ <div className={css.groupHeader}>
135
+ <span className={css.groupLabel}>{group.label}</span>
136
+ {group.note !== undefined && <span className={css.groupNote}>{group.note}</span>}
137
+ </div>
138
+ <div className={css.groupItems}>
139
+ {categories.map((cat) => (
140
+ <Checkbox
141
+ key={cat.id}
142
+ checked={selection.includes(cat.id)}
143
+ onChange={(checked) => { toggleSection(cat.id, checked) }}
144
+ label={
145
+ <span className={css.categoryItem}>
146
+ <span className={css.categoryName}>{cat.label}</span>
147
+ <span className={css.categoryDesc}>{cat.description}</span>
148
+ {cat.portability !== 'portable' && (
149
+ <Badge kind={cat.portability === 'deviceSpecific' ? 'warn' : 'info'}>{cat.portability}</Badge>
150
+ )}
151
+ {cat.sensitive === true && <Badge kind="warn">secret</Badge>}
152
+ </span>
153
+ }
154
+ />
155
+ ))}
156
+ </div>
157
+ </Card>
158
+ )
159
+ })}
160
+ </div>
161
+ )}
162
+
163
+ {/* 设备相关分区警告 */}
164
+ {customWarnings.length > 0 && (
165
+ <Banner kind="warn">
166
+ {t('export.selectionWarnings')}
167
+ <ul className={css.warnList}>
168
+ {customWarnings.map((w, i) => <li key={i}>{w}</li>)}
169
+ </ul>
170
+ </Banner>
171
+ )}
172
+
173
+ {/* 加密备份选项 */}
174
+ <Card className={css.optionsCard}>
175
+ <Checkbox
176
+ checked={includeSecrets}
177
+ onChange={setIncludeSecrets}
178
+ label={<span className={css.categoryName}>{t('export.includeSecrets')}</span>}
179
+ />
180
+ <div className={css.hint}>{t('export.includeSecretsHint')}</div>
181
+ {includeSecrets && (
182
+ <div className={css.secretFields}>
183
+ <label className={css.field}>
184
+ <span className={css.fieldLabel}>{t('export.password')}</span>
185
+ <input type="password" className={css.input} value={password} onChange={(e: ChangeEvent<HTMLInputElement>) => { setPassword(e.target.value) }} />
186
+ </label>
187
+ <label className={css.field}>
188
+ <span className={css.fieldLabel}>{t('export.passwordConfirm')}</span>
189
+ <input type="password" className={css.input} value={passwordConfirm} onChange={(e: ChangeEvent<HTMLInputElement>) => { setPasswordConfirm(e.target.value) }} />
190
+ </label>
191
+ {password !== '' && password !== passwordConfirm && (
192
+ <span className={css.formError}>{t('export.passwordMismatch')}</span>
193
+ )}
194
+ {password === '' && includeSecrets && (
195
+ <span className={css.formError}>{t('export.passwordRequired')}</span>
196
+ )}
197
+ </div>
198
+ )}
199
+ </Card>
200
+
201
+ {/* 执行 */}
202
+ <div className={css.actionRow}>
203
+ <Button variant="primary" disabled={running || passwordInvalid} onClick={() => { void runExport() }}>
204
+ {running ? <Spinner label={t('export.running')} /> : t('export.run')}
205
+ </Button>
206
+ </div>
207
+
208
+ {running && <ProgressBar event={progress} active />}
209
+
210
+ {error !== null && (
211
+ <ErrorBanner error={error} onRetry={() => { void runExport() }} retrying={running} />
212
+ )}
213
+
214
+ {result !== null && !running && (
215
+ <>
216
+ <ReportView kind="export" exportReport={result.report} onDownload={() => { void download() }} />
217
+ {downloaded && <Banner kind="ok">Saved: {result.report.file.name}</Banner>}
218
+ </>
219
+ )}
220
+ </div>
221
+ )
222
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * 冲突决策列表(规范 §11,绑 src/ui/conflict-view.ts 的 ConflictCollector)。
3
+ *
4
+ * 渲染 plan 中 kind === 'Conflict' 的项,用户逐项选择
5
+ * Keep Current / Use Imported / Review;决策实时写入 collector,
6
+ * 完成时父组件调用 collector.toResolutions() → wizard.setResolutions()。
7
+ */
8
+ import { useState } from 'react'
9
+ import { ConflictCollector } from '../../ui/conflict-view.ts'
10
+ import type { ItemResolution } from '../../core/types.ts'
11
+ import type { TranslateNS } from '../client-types.ts'
12
+ import { Banner } from '../common/ui.tsx'
13
+ import css from '../config-manager.module.css'
14
+
15
+ export interface ConflictListProps {
16
+ collector: ConflictCollector
17
+ t: TranslateNS<'config-manager'>
18
+ /** 任意决策变化后通知父组件刷新(tick) */
19
+ onChanged: () => void
20
+ }
21
+
22
+ const RESOLUTION_OPTIONS: { value: ItemResolution; key: string }[] = [
23
+ { value: 'keepCurrent', key: 'import.conflicts.keepCurrent' },
24
+ { value: 'useImported', key: 'import.conflicts.useImported' },
25
+ { value: 'review', key: 'import.conflicts.review' },
26
+ ]
27
+
28
+ /** 冲突项决策列表 */
29
+ export function ConflictList({ collector, t, onChanged }: ConflictListProps) {
30
+ const [tick, setTick] = useState(0)
31
+ const items = collector.viewItems()
32
+ const unresolved = collector.unresolved().length
33
+
34
+ return (
35
+ <div className={css.conflictList}>
36
+ {unresolved > 0 && <Banner kind="warn">{t('import.conflicts.unresolved', { count: String(unresolved) })}</Banner>}
37
+ {items.map((view) => {
38
+ const item = view.item
39
+ return (
40
+ <div key={item.id} className={css.conflictItem}>
41
+ <div className={css.conflictHead}>
42
+ <span className={css.conflictId}>{item.adapter}: {item.description}</span>
43
+ {item.severity === 'error' && <span className={css.severityError}>error</span>}
44
+ </div>
45
+ {item.detail !== undefined && item.detail !== '' && (
46
+ <pre className={css.conflictDetail}>{item.detail}</pre>
47
+ )}
48
+ <div className={css.conflictOptions}>
49
+ {RESOLUTION_OPTIONS.map((opt) => (
50
+ <label key={opt.value} className={css.radioLabel}>
51
+ <input
52
+ type="radio"
53
+ name={`conflict-${item.id}`}
54
+ checked={view.resolution === opt.value}
55
+ onChange={() => {
56
+ collector.resolve(item.id, opt.value)
57
+ setTick((v) => v + 1)
58
+ onChanged()
59
+ }}
60
+ />
61
+ {t(opt.key as 'import.conflicts.keepCurrent' | 'import.conflicts.useImported' | 'import.conflicts.review')}
62
+ </label>
63
+ ))}
64
+ </div>
65
+ </div>
66
+ )
67
+ })}
68
+ {items.length === 0 && <div className={css.empty}>No conflicts</div>}
69
+ {void tick}
70
+ </div>
71
+ )
72
+ }