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,93 @@
1
+ /**
2
+ * report 测试(m6-ui):§21 导出报告 / §22 导入报告 / §17 回滚报告 / 动作按钮。
3
+ */
4
+ import test from 'node:test';
5
+ import assert from 'node:assert/strict';
6
+ import {
7
+ formatBytes, importSectionStats, renderExportReport, renderImportReport,
8
+ renderRollbackReport, sectionFromItemId, suggestedActions,
9
+ } from './report.ts';
10
+ import { makeExportReport, makeImportResult, makeRollbackReport } from './test-helpers.ts';
11
+
12
+ test('report: sectionFromItemId 前缀推断', () => {
13
+ assert.equal(sectionFromItemId('settings:general'), 'settings');
14
+ assert.equal(sectionFromItemId('ui:theme'), 'ui');
15
+ assert.equal(sectionFromItemId('provider:pi-ai'), 'providers');
16
+ assert.equal(sectionFromItemId('plugin:x'), 'plugins');
17
+ assert.equal(sectionFromItemId('patch:line-1'), 'plugins');
18
+ assert.equal(sectionFromItemId('mcp:filesystem'), 'mcp');
19
+ assert.equal(sectionFromItemId('prompt:p1'), 'prompts');
20
+ assert.equal(sectionFromItemId('workspace:ws-1'), 'workspaces');
21
+ assert.equal(sectionFromItemId('secret:K1'), 'credentialsStatus');
22
+ assert.equal(sectionFromItemId('skills:my.md'), 'skills');
23
+ assert.equal(sectionFromItemId('sessions:s1'), 'sessions');
24
+ assert.equal(sectionFromItemId('unknown'), 'other');
25
+ });
26
+
27
+ test('report: 导出报告含 Included/Excluded/Security/File', () => {
28
+ const text = renderExportReport(makeExportReport());
29
+ assert.ok(text.includes('Backup Created'));
30
+ assert.ok(text.includes('✓ settings'));
31
+ assert.ok(text.includes('8 plugins'));
32
+ assert.ok(text.includes('○ sessions'));
33
+ assert.ok(text.includes('API Keys excluded: yes'));
34
+ assert.ok(text.includes('File: dsh-config-2026-08-14.zip'));
35
+ assert.ok(text.includes('2 sensitive field(s) redacted'));
36
+ });
37
+
38
+ test('report: 导入报告分节统计', () => {
39
+ const result = makeImportResult();
40
+ const stats = importSectionStats(result.executed);
41
+ const settings = stats.find((s) => s.section === 'settings')!;
42
+ assert.equal(settings.ok, 2);
43
+ const plugins = stats.find((s) => s.section === 'plugins')!;
44
+ assert.equal(plugins.ok, 1);
45
+ assert.equal(plugins.skipped, 1);
46
+ const secrets = stats.find((s) => s.section === 'credentialsStatus')!;
47
+ assert.equal(secrets.skipped, 1);
48
+ });
49
+
50
+ test('report: 导入报告渲染含分节/重启/缺失 secret', () => {
51
+ const text = renderImportReport(makeImportResult());
52
+ assert.ok(text.includes('Import Complete'));
53
+ assert.ok(text.includes('settings: ✓ 2 imported'));
54
+ assert.ok(text.includes('Secrets: ⚠ 1 credentials need to be entered'));
55
+ assert.ok(text.includes('Restart required'));
56
+ });
57
+
58
+ test('report: 失败导入渲染 Import Failed + 回滚报告', () => {
59
+ const result = makeImportResult({
60
+ ok: false,
61
+ executed: [
62
+ { itemId: 'settings:a', status: 'ok' },
63
+ { itemId: 'mcp:m', status: 'failed', message: 'npx not found' },
64
+ ],
65
+ rollback: makeRollbackReport(),
66
+ });
67
+ const text = renderImportReport(result);
68
+ assert.ok(text.includes('Import Failed'));
69
+ assert.ok(text.includes('Rollback partially completed.'));
70
+ assert.ok(text.includes('manual recovery'));
71
+ assert.ok(text.includes('npx not found'));
72
+ });
73
+
74
+ test('report: 回滚报告 full / partial', () => {
75
+ assert.ok(renderRollbackReport({ full: true, restored: [], failed: [] }).includes('完整恢复'));
76
+ const partial = renderRollbackReport(makeRollbackReport());
77
+ assert.ok(partial.includes('Rollback partially completed.'));
78
+ assert.ok(partial.includes('请手动安装'));
79
+ });
80
+
81
+ test('report: suggestedActions 顺序(fixIssues → viewDetails → done)', () => {
82
+ assert.deepEqual(suggestedActions(makeImportResult()), ['viewDetails', 'done']);
83
+ const failed = makeImportResult({
84
+ executed: [{ itemId: 'mcp:m', status: 'failed' }],
85
+ });
86
+ assert.deepEqual(suggestedActions(failed), ['fixIssues', 'viewDetails', 'done']);
87
+ });
88
+
89
+ test('report: formatBytes', () => {
90
+ assert.equal(formatBytes(512), '512 B');
91
+ assert.equal(formatBytes(20480), '20.0 KB');
92
+ assert.equal(formatBytes(2 * 1024 * 1024), '2.0 MB');
93
+ });
@@ -0,0 +1,140 @@
1
+ /**
2
+ * 报告渲染(规范 §21 导出报告 / §22 导入报告 / §17 回滚报告,m6-ui)。
3
+ *
4
+ * 纯文本渲染:输出用户可读的多行文本(未来 React 客户端可直接替换为组件渲染)。
5
+ * 导入报告按分区统计:从 ExecutedItem.itemId 前缀推断所属分区(与 adapters 的 id 规则一致)。
6
+ */
7
+ import type { SectionId } from '../schema/types.ts';
8
+ import type {
9
+ ExecutedItem, ExportReport, ImportResult, RollbackReport,
10
+ } from '../core/types.ts';
11
+ import type { ImportResultAction, ImportSectionStat } from './types.ts';
12
+
13
+ /* ---------------- §21 导出报告 ---------------- */
14
+
15
+ export function renderExportReport(report: ExportReport): string {
16
+ const lines: string[] = ['Backup Created', ''];
17
+ lines.push('Included:');
18
+ for (const { section, counts } of report.included) {
19
+ const detail = Object.entries(counts)
20
+ .map(([k, v]) => `${v} ${k}`)
21
+ .join(', ');
22
+ lines.push(` ✓ ${section}${detail !== '' ? ` (${detail})` : ''}`);
23
+ }
24
+ lines.push('');
25
+ if (report.excluded.length > 0) {
26
+ lines.push('Excluded:');
27
+ for (const s of report.excluded) lines.push(` ○ ${s}`);
28
+ lines.push('');
29
+ }
30
+ lines.push('Security:');
31
+ lines.push(` ✓ API Keys excluded: ${report.security.secretsExcluded ? 'yes' : 'no'}`);
32
+ lines.push(` ✓ Contains secrets: ${report.security.containsSecrets ? 'yes (encrypted)' : 'no'}`);
33
+ lines.push(` ✓ Encrypted: ${report.security.encrypted ? 'yes' : 'no'}`);
34
+ if (report.security.redactedHits > 0) lines.push(` ⚠ ${report.security.redactedHits} sensitive field(s) redacted`);
35
+ lines.push('');
36
+ lines.push(`File: ${report.file.name} (${formatBytes(report.file.sizeBytes)})`);
37
+ for (const w of report.warnings) lines.push(` ⚠ ${w}`);
38
+ return lines.join('\n');
39
+ }
40
+
41
+ /* ---------------- §22 导入报告 ---------------- */
42
+
43
+ /** 从 itemId 前缀推断所属分区(与 adapters id 规则对齐;未知归 'other') */
44
+ export function sectionFromItemId(itemId: string): SectionId | 'other' {
45
+ if (itemId.startsWith('settings:')) return 'settings';
46
+ if (itemId.startsWith('ui:')) return 'ui';
47
+ if (itemId.startsWith('provider:')) return 'providers';
48
+ if (itemId.startsWith('plugin:') || itemId.startsWith('patch:')) return 'plugins';
49
+ if (itemId.startsWith('mcp:')) return 'mcp';
50
+ if (itemId.startsWith('prompt:')) return 'prompts';
51
+ if (itemId.startsWith('workspace:')) return 'workspaces';
52
+ if (itemId.startsWith('secret:')) return 'credentialsStatus';
53
+ if (itemId.startsWith('skills:')) return 'skills';
54
+ if (itemId.startsWith('agentPresets:')) return 'agentPresets';
55
+ if (itemId.startsWith('pluginFiles:')) return 'pluginFiles';
56
+ if (itemId.startsWith('sessions:')) return 'sessions';
57
+ return 'other';
58
+ }
59
+
60
+ /** 按分区聚合执行结果(统计 + 明细) */
61
+ export function importSectionStats(executed: readonly ExecutedItem[]): ImportSectionStat[] {
62
+ const bySection = new Map<string, ImportSectionStat>();
63
+ for (const e of executed) {
64
+ const section = sectionFromItemId(e.itemId);
65
+ let stat = bySection.get(section);
66
+ if (!stat) {
67
+ stat = { section: section as SectionId, ok: 0, skipped: 0, failed: 0, items: [] };
68
+ bySection.set(section, stat);
69
+ }
70
+ if (e.status === 'ok') stat.ok += 1;
71
+ else if (e.status === 'skipped') stat.skipped += 1;
72
+ else stat.failed += 1;
73
+ stat.items.push(e);
74
+ }
75
+ return [...bySection.values()];
76
+ }
77
+
78
+ /** 导入报告渲染(含回滚状态;§22 动作按钮由 suggestedActions 给出) */
79
+ export function renderImportReport(result: ImportResult): string {
80
+ const lines: string[] = [result.ok ? 'Import Complete' : 'Import Failed', ''];
81
+ const stats = importSectionStats(result.executed);
82
+ for (const s of stats) {
83
+ const parts: string[] = [];
84
+ if (s.ok > 0) parts.push(`✓ ${s.ok} imported/restored`);
85
+ if (s.skipped > 0) parts.push(`- ${s.skipped} skipped`);
86
+ if (s.failed > 0) parts.push(`✗ ${s.failed} failed`);
87
+ lines.push(`${s.section}: ${parts.join(' ') || 'no changes'}`);
88
+ // 失败项给出可操作原因(§23)
89
+ for (const it of s.items) {
90
+ if (it.status === 'failed') {
91
+ lines.push(` Reason: ${it.message ?? '未知原因'}`);
92
+ }
93
+ }
94
+ }
95
+ if (result.missingSecrets.length > 0) {
96
+ lines.push(`Secrets: ⚠ ${result.missingSecrets.length} credentials need to be entered`);
97
+ }
98
+ if (result.needsRestart) {
99
+ lines.push('Restart required: 插件/MCP 变更将在重启 DSH 后生效');
100
+ }
101
+ for (const w of result.warnings) lines.push(`⚠ ${w}`);
102
+ if (result.rollback) {
103
+ lines.push('');
104
+ lines.push(renderRollbackReport(result.rollback));
105
+ }
106
+ return lines.join('\n');
107
+ }
108
+
109
+ /** 结果页动作按钮(§22:Fix Issues / View Details / Done) */
110
+ export function suggestedActions(result: ImportResult): ImportResultAction[] {
111
+ const actions: ImportResultAction[] = [];
112
+ if (result.executed.some((e) => e.status === 'failed')) actions.push('fixIssues');
113
+ if (result.executed.length > 0 || result.warnings.length > 0) actions.push('viewDetails');
114
+ actions.push('done');
115
+ return actions;
116
+ }
117
+
118
+ /* ---------------- §17 回滚报告 ---------------- */
119
+
120
+ /** 回滚报告渲染(full / partial + 人工恢复清单) */
121
+ export function renderRollbackReport(rr: RollbackReport): string {
122
+ if (rr.full) {
123
+ return 'Rollback: 已完整恢复导入前的配置。';
124
+ }
125
+ const lines = ['Rollback partially completed.'];
126
+ lines.push('These items may require manual recovery:');
127
+ for (const f of rr.failed) {
128
+ lines.push(` - ${f.item}: ${f.reason}${f.manualHint ? ` (${f.manualHint})` : ''}`);
129
+ }
130
+ if (rr.restored.length > 0) lines.push(`Restored: ${rr.restored.length} item(s)`);
131
+ return lines.join('\n');
132
+ }
133
+
134
+ /* ---------------- 工具 ---------------- */
135
+
136
+ export function formatBytes(n: number): string {
137
+ if (n < 1024) return `${n} B`;
138
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
139
+ return `${(n / (1024 * 1024)).toFixed(1)} MB`;
140
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * UI 层测试辅助(m6-ui):mock core 调用与样本数据。
3
+ * 仅供 src/ui/*.test.ts 使用,不参与生产构建语义。
4
+ */
5
+ import type { Manifest } from '../schema/types.ts';
6
+ import type {
7
+ ExportReport, ImportAnalysis, ImportDecisions, ImportPlan, ImportResult,
8
+ PlanItem, RollbackReport,
9
+ } from '../core/types.ts';
10
+ import type { ExportPort } from './export-flow.ts';
11
+ import type { ImportPort } from './types.ts';
12
+
13
+ /** 样本 manifest(UI 测试不关心真实字段,仅类型占位) */
14
+ export function makeManifest(): Manifest {
15
+ return {
16
+ schemaVersion: 1,
17
+ exporter: { name: 'dsh-config-manager-test', version: '0.0.0' },
18
+ source: { dshVersion: '0.1.0-rc.6', platform: 'win32', arch: 'x64' },
19
+ exportedAt: '2026-08-14T12:00:00.000Z',
20
+ sections: {
21
+ settings: true, ui: false, providers: false, plugins: false, mcp: false, prompts: false,
22
+ skills: false, agentPresets: false, workspaces: false, pluginFiles: false,
23
+ credentialsStatus: false, secrets: false, sessions: false,
24
+ },
25
+ security: { containsSecrets: false, encrypted: false, encryption: null },
26
+ };
27
+ }
28
+
29
+ export function makeExportReport(overrides: Partial<ExportReport> = {}): ExportReport {
30
+ return {
31
+ included: [
32
+ { section: 'settings', counts: { namespaces: 3 } },
33
+ { section: 'plugins', counts: { plugins: 8 } },
34
+ ],
35
+ excluded: ['sessions', 'pluginFiles'],
36
+ security: { secretsExcluded: true, containsSecrets: false, encrypted: false, redactedHits: 2 },
37
+ file: { name: 'dsh-config-2026-08-14.zip', sizeBytes: 20480 },
38
+ warnings: [],
39
+ ...overrides,
40
+ };
41
+ }
42
+
43
+ /** 内存 ExportPort mock:记录调用参数,返回固定结果 */
44
+ export class MockExportPort implements ExportPort {
45
+ calls: { includeSecrets: boolean; only?: string[] }[] = [];
46
+ result: { zipPath: string; manifest: Manifest; report: ExportReport };
47
+ constructor(report: ExportReport = makeExportReport()) {
48
+ this.result = {
49
+ zipPath: report.file.name,
50
+ manifest: makeManifest(),
51
+ report,
52
+ };
53
+ }
54
+ async export(opts: { includeSecrets: boolean; only?: string[] }): Promise<{ zipPath: string; manifest: Manifest; report: ExportReport }> {
55
+ this.calls.push({ includeSecrets: opts.includeSecrets, only: opts.only });
56
+ return this.result;
57
+ }
58
+ }
59
+
60
+ /* ---------------- Import mock ---------------- */
61
+
62
+ export function makeAnalysis(overrides: Partial<ImportAnalysis> = {}): ImportAnalysis {
63
+ return {
64
+ valid: true,
65
+ errors: [],
66
+ warnings: [],
67
+ compatibility: 'good',
68
+ sectionsInZip: ['settings', 'plugins', 'mcp', 'prompts'],
69
+ pluginSummary: { installed: 6, toInstall: 2 },
70
+ pathIssues: [],
71
+ secretCount: 3,
72
+ dependencyIssues: [],
73
+ ...overrides,
74
+ };
75
+ }
76
+
77
+ export function makePlanItem(overrides: Partial<PlanItem> = {}): PlanItem {
78
+ return {
79
+ id: 'settings:general',
80
+ kind: 'Create',
81
+ adapter: 'settings',
82
+ description: '创建设置 general',
83
+ severity: 'info',
84
+ ...overrides,
85
+ };
86
+ }
87
+
88
+ export function makePlan(overrides: Partial<ImportPlan> = {}): ImportPlan {
89
+ return {
90
+ items: [
91
+ makePlanItem({ id: 'settings:a', kind: 'Update', description: '更新设置 a' }),
92
+ makePlanItem({ id: 'settings:b', kind: 'Create', description: '创建设置 b' }),
93
+ makePlanItem({ id: 'plugin:x', kind: 'Install', adapter: 'plugins', description: '安装插件 x' }),
94
+ makePlanItem({ id: 'plugin:y', kind: 'Skip', adapter: 'plugins', description: '插件 y 已一致' }),
95
+ makePlanItem({ id: 'prompt:p', kind: 'Create', adapter: 'prompts', description: '创建提示 p' }),
96
+ makePlanItem({ id: 'mcp:m', kind: 'Create', adapter: 'mcp', description: '创建 MCP m' }),
97
+ makePlanItem({ id: 'secret:K1', kind: 'MissingSecret', adapter: 'credentialsStatus', description: '凭据 K1 需要补录' }),
98
+ ],
99
+ globalStrategy: 'merge',
100
+ pathMappings: [],
101
+ missingSecrets: [{ ref: 'K1', required: true }],
102
+ needsRestart: true,
103
+ estimatedActions: {
104
+ settings: 2, ui: 0, providers: 0, plugins: 1, mcp: 1, prompts: 1,
105
+ skills: 0, agentPresets: 0, workspaces: 0, pluginFiles: 0,
106
+ credentialsStatus: 0, secrets: 0, sessions: 0,
107
+ },
108
+ ...overrides,
109
+ };
110
+ }
111
+
112
+ export function makeRollbackReport(overrides: Partial<RollbackReport> = {}): RollbackReport {
113
+ return {
114
+ full: false,
115
+ restored: ['settings:a'],
116
+ failed: [{ item: 'plugin:x', reason: '安装失败', manualHint: '请手动安装' }],
117
+ ...overrides,
118
+ };
119
+ }
120
+
121
+ export function makeImportResult(overrides: Partial<ImportResult> = {}): ImportResult {
122
+ return {
123
+ ok: true,
124
+ executed: [
125
+ { itemId: 'settings:a', status: 'ok' },
126
+ { itemId: 'settings:b', status: 'ok' },
127
+ { itemId: 'plugin:x', status: 'ok', message: '需要重启生效' },
128
+ { itemId: 'plugin:y', status: 'skipped' },
129
+ { itemId: 'prompt:p', status: 'ok' },
130
+ { itemId: 'mcp:m', status: 'ok' },
131
+ { itemId: 'secret:K1', status: 'skipped', message: '凭据未提供' },
132
+ ],
133
+ needsRestart: true,
134
+ missingSecrets: ['K1'],
135
+ warnings: [],
136
+ rollback: null,
137
+ snapshotId: 'snap-1',
138
+ ...overrides,
139
+ };
140
+ }
141
+
142
+ /** 内存 ImportPort mock:可编排各阶段返回与调用记录 */
143
+ export class MockImportPort implements ImportPort {
144
+ analysis: ImportAnalysis;
145
+ plan: ImportPlan;
146
+ result: ImportResult;
147
+ analyzeCalls = 0;
148
+ planCalls: ImportDecisions[] = [];
149
+ executeCalls: { confirm: boolean; secretInputs?: Record<string, string>; rollbackOnError: boolean }[] = [];
150
+
151
+ constructor(opts: {
152
+ analysis?: ImportAnalysis;
153
+ plan?: ImportPlan;
154
+ result?: ImportResult;
155
+ } = {}) {
156
+ this.analysis = opts.analysis ?? makeAnalysis();
157
+ this.plan = opts.plan ?? makePlan();
158
+ this.result = opts.result ?? makeImportResult();
159
+ }
160
+
161
+ async analyzeImport(): Promise<ImportAnalysis> {
162
+ this.analyzeCalls += 1;
163
+ return this.analysis;
164
+ }
165
+ async createImportPlan(_zip: string, decisions: ImportDecisions): Promise<ImportPlan> {
166
+ this.planCalls.push(decisions);
167
+ return this.plan;
168
+ }
169
+ async executeImportPlan(
170
+ _zip: string,
171
+ _plan: ImportPlan,
172
+ opts: { confirm: boolean; secretInputs?: Record<string, string>; rollbackOnError: boolean },
173
+ ): Promise<ImportResult> {
174
+ this.executeCalls.push(opts);
175
+ return this.result;
176
+ }
177
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * UI 层共享类型(m6-ui,框架无关)。
3
+ *
4
+ * 设计原则(对齐 Docs/design/architecture.md §12.2 与规范 §28/§29):
5
+ * - 本模块为纯 TypeScript 逻辑层,不引入 React / 打包器 / 任何运行时依赖;
6
+ * - 未来 React 客户端(src/client/)直接消费本层的控制器输出(渲染文本 / 状态快照);
7
+ * - UI 层不直接 import core 的类实现,只依赖 core/types.ts 的纯类型 + 注入的「端口」
8
+ * (ExportPort / ImportPort,宿主在 settings.section 挂载时把真实 Exporter/Importer 接入)。
9
+ */
10
+ import type { SectionId } from '../schema/types.ts';
11
+ import type {
12
+ CompatibilityScore, ImportAnalysis, ImportDecisions, ImportPlan, ImportResult,
13
+ ItemResolution, PathIssue, PlanItem, Portability, RollbackReport,
14
+ } from '../core/types.ts';
15
+
16
+ /* ---------------- 导出(规范 §1 / §21) ---------------- */
17
+
18
+ export type ExportMode = 'quick' | 'custom';
19
+
20
+ /** Custom Export 分组(规范 §1 分类;automation 组在 DSH 中无对应分区,UI 标注说明) */
21
+ export type ExportGroup =
22
+ | 'general' | 'ai' | 'extensions' | 'mcp' | 'customization'
23
+ | 'automation' | 'workspace' | 'ui' | 'optional';
24
+
25
+ /** 分类树节点(与 adapters 的 displayName/defaultIncluded/portability 对齐;宿主可注入覆盖) */
26
+ export interface ExportCategory {
27
+ id: SectionId;
28
+ label: string;
29
+ description: string;
30
+ defaultIncluded: boolean;
31
+ portability: Portability;
32
+ group: ExportGroup;
33
+ /** 涉及秘密状态(如 credentialsStatus):UI 需展示安全提示但绝不显示值 */
34
+ sensitive?: boolean;
35
+ }
36
+
37
+ export interface ExportGroupDef {
38
+ id: ExportGroup;
39
+ label: string;
40
+ /** automation 组在 DSH 无对应配置时的说明 */
41
+ note?: string;
42
+ }
43
+
44
+ /** Custom Export 分组目录(规范 §1;automation 组 DSH 无对应分区,仅说明) */
45
+ export const EXPORT_GROUPS: readonly ExportGroupDef[] = [
46
+ { id: 'general', label: 'General' },
47
+ { id: 'ai', label: 'AI' },
48
+ { id: 'extensions', label: 'Extensions' },
49
+ { id: 'mcp', label: 'MCP / Tools' },
50
+ { id: 'customization', label: 'Customization' },
51
+ { id: 'automation', label: 'Automation', note: 'DSH 当前无 Workflows / Commands 配置文件(运行时注册),无迁移内容' },
52
+ { id: 'workspace', label: 'Workspace' },
53
+ { id: 'ui', label: 'UI' },
54
+ { id: 'optional', label: 'Optional Data' },
55
+ ] as const;
56
+
57
+ /** Quick Export 推荐项 = defaultIncluded 且非 deviceSpecific(设计 §11.1) */
58
+ export function isQuickRecommended(c: ExportCategory): boolean {
59
+ return c.defaultIncluded && c.portability !== 'deviceSpecific';
60
+ }
61
+
62
+ /* ---------------- 进度事件(规范 §29) ---------------- */
63
+
64
+ export interface ProgressEvent {
65
+ /** 阶段 id(见 progress.ts 的 STAGE_TEXTS) */
66
+ stage: string;
67
+ detail?: string;
68
+ /** 阶段序号(从 1 起,供 UI 显示 n/m) */
69
+ step?: number;
70
+ /** 阶段总数 */
71
+ total?: number;
72
+ }
73
+
74
+ export type ProgressListener = (event: ProgressEvent) => void;
75
+
76
+ /* ---------------- 导入向导(规范 §9 / §10 / §28) ---------------- */
77
+
78
+ export type ImportStep =
79
+ | 'select' // 选 ZIP
80
+ | 'analyzing' // Analyzing...
81
+ | 'compatibility' // Compatibility
82
+ | 'preview' // Import Preview(Dry Run 摘要)
83
+ | 'conflicts' // Resolve Conflicts(§11)
84
+ | 'path-mapping' // Path Mapping(§12)
85
+ | 'secrets' // Secrets 补录(§7)
86
+ | 'importing' // Importing(含快照/回滚阶段)
87
+ | 'result'; // Result(§22)
88
+
89
+ /** Preview 摘要(规范 §10 示例的数值化版本) */
90
+ export interface ImportPreviewSummary {
91
+ /** 将更新的配置项数(Create+Update+Install 合计) */
92
+ willChange: number;
93
+ /** 已存在且一致的项(Skip) */
94
+ unchanged: number;
95
+ settingsUpdates: number;
96
+ pluginsInstalled: number;
97
+ pluginsToInstall: number;
98
+ mcpAdds: number;
99
+ prompts: number;
100
+ pathMappingsNeeded: number;
101
+ secretsNeeded: number;
102
+ conflicts: number;
103
+ needsRestart: boolean;
104
+ }
105
+
106
+ /** 向导状态快照(React 客户端可直接绑定渲染) */
107
+ export interface WizardSnapshot {
108
+ step: ImportStep;
109
+ zipPath: string | null;
110
+ analysis: ImportAnalysis | null;
111
+ plan: ImportPlan | null;
112
+ result: ImportResult | null;
113
+ /** 导入执行的回滚策略(场景 E:默认 true 整体回滚) */
114
+ rollbackOnError: boolean;
115
+ errors: string[];
116
+ }
117
+
118
+ /* ---------------- 冲突视图(规范 §11) ---------------- */
119
+
120
+ /** 单个冲突项的视图数据:当前值 vs 导入值摘要 + 用户决策 */
121
+ export interface ConflictViewItem {
122
+ item: PlanItem;
123
+ currentSummary?: string;
124
+ importedSummary?: string;
125
+ resolution: ItemResolution | null;
126
+ }
127
+
128
+ /* ---------------- 路径映射(规范 §12) ---------------- */
129
+
130
+ /** 路径映射编辑器的单条记录(旧前缀 → 新前缀;未解析时 newPrefix 为空串) */
131
+ export interface PathMappingDraft {
132
+ /** 关联的原始路径问题值(一条 issue 一条 draft;批量映射用 oldPrefix 聚合) */
133
+ oldPrefix: string;
134
+ newPrefix: string;
135
+ issue?: PathIssue;
136
+ /** 应用范围(缺省 [] = 全部相关分区,与 core applyMappingsToSections 语义一致) */
137
+ appliesTo: PathMappingAppliesTo[];
138
+ }
139
+
140
+ /** 与 core PathMapping.appliesTo 对齐的取值(留空数组 = 全应用) */
141
+ export type PathMappingAppliesTo = 'workspaces' | 'mcp' | 'pluginConfig' | 'skills';
142
+
143
+ /* ---------------- 兼容性(规范 §30) ---------------- */
144
+
145
+ export interface CompatibilityView {
146
+ score: CompatibilityScore;
147
+ sourceDsh: string;
148
+ targetDsh: string;
149
+ description: string;
150
+ }
151
+
152
+ /* ---------------- 报告(规范 §21 / §22 / §17) ---------------- */
153
+
154
+ /** 导入结果按分区的统计(report.ts 用) */
155
+ export interface ImportSectionStat {
156
+ section: SectionId;
157
+ ok: number;
158
+ skipped: number;
159
+ failed: number;
160
+ items: { itemId: string; status: 'ok' | 'skipped' | 'failed'; message?: string }[];
161
+ }
162
+
163
+ export type ImportResultAction = 'fixIssues' | 'viewDetails' | 'done';
164
+
165
+ export interface RollbackView {
166
+ report: RollbackReport | null;
167
+ /** 是否有可展示的人工恢复清单(partial 回滚时) */
168
+ hasManualRecovery: boolean;
169
+ }
170
+
171
+ /* ---------------- 控制器公共依赖注入 ---------------- */
172
+
173
+ /** UI 层与 core 的导入端口(宿主注入真实 Importer;测试注入内存 mock) */
174
+ export interface ImportPort {
175
+ analyzeImport(zipPath: string): Promise<ImportAnalysis>;
176
+ createImportPlan(zipPath: string, decisions: ImportDecisions): Promise<ImportPlan>;
177
+ executeImportPlan(
178
+ zipPath: string,
179
+ plan: ImportPlan,
180
+ opts: {
181
+ /** 用户确认(安全阀,非 true 拒绝执行) */
182
+ confirm: boolean;
183
+ secretInputs?: Record<string, string>;
184
+ /** 显式回滚策略:true=任一项失败整体回滚(场景 E);false=单项失败继续(§34.17) */
185
+ rollbackOnError: boolean;
186
+ },
187
+ ): Promise<ImportResult>;
188
+ }
189
+
190
+ /** 选项构造辅助:从 PlanItem 提取稳定决策键(与 core analyzer.applyItemResolution 的 id 语义一致) */
191
+ export function itemDecisionKey(item: PlanItem): string {
192
+ return item.id;
193
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * SHA-256 完整性工具(integrity/checksums.json 的生成与校验)。
3
+ * m4 的 security/integrity.ts 可在此之上强化(本模块已实现规范 §18 全部语义)。
4
+ */
5
+ import crypto from 'node:crypto';
6
+ import fs from 'node:fs/promises';
7
+
8
+ export function sha256Hex(data: Uint8Array | string): string {
9
+ return crypto.createHash('sha256').update(data).digest('hex');
10
+ }
11
+
12
+ export async function hashFile(filePath: string): Promise<string> {
13
+ const data = await fs.readFile(filePath);
14
+ return sha256Hex(data);
15
+ }
16
+
17
+ /** 生成一组 ZIP 相对路径 → SHA-256 的校验表(路径必须为 ZIP 内正斜杠相对路径) */
18
+ export function buildChecksums(entries: { name: string; data: Uint8Array }[]): Record<string, string> {
19
+ const checksums: Record<string, string> = {};
20
+ for (const e of entries) {
21
+ checksums[e.name] = sha256Hex(e.data);
22
+ }
23
+ return checksums;
24
+ }
25
+
26
+ export interface ChecksumVerifyResult {
27
+ ok: boolean;
28
+ mismatches: string[];
29
+ missing: string[];
30
+ }
31
+
32
+ /**
33
+ * 校验:checksums 表内每个相对路径必须在 entries 中存在且 SHA-256 一致。
34
+ * entries 中多余的文件(如 manifest.json / checksums.json 自身)不参与校验。
35
+ */
36
+ export function verifyChecksums(
37
+ entries: ReadonlyMap<string, Uint8Array>,
38
+ checksums: Record<string, string>,
39
+ ): ChecksumVerifyResult {
40
+ const mismatches: string[] = [];
41
+ const missing: string[] = [];
42
+ for (const [relPath, expected] of Object.entries(checksums)) {
43
+ const data = entries.get(relPath);
44
+ if (data === undefined) {
45
+ missing.push(relPath);
46
+ continue;
47
+ }
48
+ if (sha256Hex(data) !== expected) {
49
+ mismatches.push(relPath);
50
+ }
51
+ }
52
+ return { ok: mismatches.length === 0 && missing.length === 0, mismatches, missing };
53
+ }