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,128 @@
1
+ /**
2
+ * import-wizard 测试(m6-ui):
3
+ * - 步骤状态机 select→analyzing→compatibility→preview→importing→result
4
+ * - 显式 rollbackOnError(场景 E 默认 true)
5
+ * - Preview 摘要(§10)
6
+ * - confirm 安全阀、进度事件、reset
7
+ */
8
+ import test from 'node:test';
9
+ import assert from 'node:assert/strict';
10
+ import { ImportWizard } from './import-wizard.ts';
11
+ import { MockImportPort, makeAnalysis, makeImportResult, makePlan } from './test-helpers.ts';
12
+
13
+ test('import-wizard: selectZip 进入 compatibility 并保存 analysis', async () => {
14
+ const port = new MockImportPort();
15
+ const wiz = new ImportWizard({ port });
16
+ assert.equal(wiz.currentStep, 'select');
17
+
18
+ const analysis = await wiz.selectZip('C:\\backup\\dsh-config.zip');
19
+ assert.equal(port.analyzeCalls, 1);
20
+ assert.equal(analysis.compatibility, 'good');
21
+ assert.equal(wiz.currentStep, 'compatibility');
22
+ assert.equal(wiz.snapshot().zipPath, 'C:\\backup\\dsh-config.zip');
23
+ });
24
+
25
+ test('import-wizard: 分析失败进入 errors 并抛出', async () => {
26
+ const port = new MockImportPort();
27
+ port.analysis = makeAnalysis({ valid: false, errors: ['备份完整性校验失败'] });
28
+ const wiz = new ImportWizard({ port });
29
+ await assert.rejects(() => wiz.selectZip('x.zip'), /完整性/);
30
+ assert.equal(wiz.snapshot().errors.length, 1);
31
+ });
32
+
33
+ test('import-wizard: confirmCompatibility 生成计划并进入 preview', async () => {
34
+ const port = new MockImportPort();
35
+ const wiz = new ImportWizard({ port });
36
+ await wiz.selectZip('x.zip');
37
+ const plan = await wiz.confirmCompatibility();
38
+ assert.equal(wiz.currentStep, 'preview');
39
+ assert.ok(plan.items.length > 0);
40
+ assert.equal(port.planCalls.length, 1);
41
+ });
42
+
43
+ test('import-wizard: Preview 摘要统计(§10 数值化)', async () => {
44
+ const port = new MockImportPort();
45
+ const wiz = new ImportWizard({ port });
46
+ await wiz.selectZip('x.zip');
47
+ await wiz.confirmCompatibility();
48
+ const s = wiz.previewSummary();
49
+ assert.equal(s.willChange, 5); // Create×3 + Update×1 + Install×1(Skip/MissingSecret 不计)
50
+ assert.equal(s.pluginsToInstall, 1);
51
+ assert.equal(s.mcpAdds, 1);
52
+ assert.equal(s.prompts, 1);
53
+ assert.equal(s.conflicts, 0);
54
+ assert.equal(s.secretsNeeded, 1);
55
+ assert.equal(s.needsRestart, true);
56
+ });
57
+
58
+ test('import-wizard: execute 默认显式传 rollbackOnError=true(场景 E)', async () => {
59
+ const port = new MockImportPort();
60
+ const wiz = new ImportWizard({ port });
61
+ await wiz.selectZip('x.zip');
62
+ await wiz.confirmCompatibility();
63
+ const result = await wiz.execute({ confirm: true });
64
+
65
+ assert.equal(result.ok, true);
66
+ assert.equal(wiz.currentStep, 'result');
67
+ assert.equal(port.executeCalls.length, 1);
68
+ assert.equal(port.executeCalls[0]!.confirm, true);
69
+ assert.equal(port.executeCalls[0]!.rollbackOnError, true, '默认整体回滚');
70
+ });
71
+
72
+ test('import-wizard: 用户可选 rollbackOnError=false(单项失败继续 §34.17)', async () => {
73
+ const port = new MockImportPort();
74
+ const wiz = new ImportWizard({ port });
75
+ await wiz.selectZip('x.zip');
76
+ await wiz.confirmCompatibility();
77
+ await wiz.execute({ confirm: true, rollbackOnError: false });
78
+ assert.equal(port.executeCalls[0]!.rollbackOnError, false);
79
+ });
80
+
81
+ test('import-wizard: confirm=false 拒绝执行(core 安全阀透传)', async () => {
82
+ const port = new MockImportPort();
83
+ const wiz = new ImportWizard({ port });
84
+ await wiz.selectZip('x.zip');
85
+ await wiz.confirmCompatibility();
86
+ await assert.rejects(() => wiz.execute({ confirm: false }), /未确认/);
87
+ assert.equal(port.executeCalls.length, 0);
88
+ });
89
+
90
+ test('import-wizard: secretInputs 仅内存传递', async () => {
91
+ const port = new MockImportPort();
92
+ const wiz = new ImportWizard({ port });
93
+ await wiz.selectZip('x.zip');
94
+ await wiz.confirmCompatibility();
95
+ wiz.setSecretInputs({ K1: 'sk-xxx' });
96
+ await wiz.execute({ confirm: true });
97
+ assert.equal(port.executeCalls[0]!.secretInputs?.['K1'], 'sk-xxx');
98
+ });
99
+
100
+ test('import-wizard: 失败且回滚时发出 rolling-back 进度事件', async () => {
101
+ const port = new MockImportPort();
102
+ port.result = makeImportResult({
103
+ ok: false,
104
+ rollback: { full: true, restored: ['settings:a'], failed: [] },
105
+ });
106
+ const events: string[] = [];
107
+ const wiz = new ImportWizard({ port, onProgress: (e) => events.push(e.stage) });
108
+ await wiz.selectZip('x.zip');
109
+ await wiz.confirmCompatibility();
110
+ const result = await wiz.execute({ confirm: true });
111
+ assert.equal(result.ok, false);
112
+ assert.ok(events.includes('rolling-back'));
113
+ assert.ok(events.includes('done'));
114
+ });
115
+
116
+ test('import-wizard: reset 清空状态回 select', async () => {
117
+ const port = new MockImportPort();
118
+ const wiz = new ImportWizard({ port });
119
+ await wiz.selectZip('x.zip');
120
+ await wiz.confirmCompatibility();
121
+ await wiz.execute({ confirm: true });
122
+ wiz.reset();
123
+ const snap = wiz.snapshot();
124
+ assert.equal(snap.step, 'select');
125
+ assert.equal(snap.zipPath, null);
126
+ assert.equal(snap.plan, null);
127
+ assert.equal(snap.result, null);
128
+ });
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Import 向导状态机(规范 §9/§10/§28,m6-ui)。
3
+ *
4
+ * 步骤:Select ZIP → Analyzing → Compatibility → Import Preview → Resolve Conflicts
5
+ * → Path Mapping → Secrets → Importing → Result。
6
+ *
7
+ * 关键安全/正确性约束:
8
+ * - analyzeImport / createImportPlan 零写入(Dry Run 复用,规范 §10);
9
+ * - executeImportPlan 必须 confirm=true(core 安全阀),UI 在用户点「确认导入」后传入;
10
+ * - **显式传 rollbackOnError**:场景 E(§36)要求导入中途失败整体回滚,
11
+ * 本向导默认 rollbackOnError=true,UI 可在执行前让用户选择(true=整体回滚 / false=单项失败继续 §34.17);
12
+ * - 秘密补录值仅内存(secretInputs),绝不落日志/落盘。
13
+ */
14
+ import type {
15
+ GlobalConflictStrategy, ImportAnalysis, ImportDecisions, ImportPlan, ImportResult,
16
+ PathMapping, PlanItem,
17
+ } from '../core/types.ts';
18
+ import type { ImportPort, ImportPreviewSummary, ImportStep, ProgressListener, WizardSnapshot } from './types.ts';
19
+ import { IMPORT_STAGES, ProgressTracker } from './progress.ts';
20
+ import { formatActionableError, toActionableError } from './errors.ts';
21
+
22
+ export interface ImportWizardOptions {
23
+ port: ImportPort;
24
+ onProgress?: ProgressListener;
25
+ /** 默认回滚策略(场景 E:true=整体回滚;UI 可在执行前覆盖) */
26
+ defaultRollbackOnError?: boolean;
27
+ }
28
+
29
+ export class ImportWizard {
30
+ private readonly port: ImportPort;
31
+ private readonly onProgress: ProgressListener | undefined;
32
+ private readonly tracker: ProgressTracker;
33
+ private step: ImportStep = 'select';
34
+ private zipPath: string | null = null;
35
+ private analysis: ImportAnalysis | null = null;
36
+ private plan: ImportPlan | null = null;
37
+ private result: ImportResult | null = null;
38
+ private rollbackOnError: boolean;
39
+ private errors: string[] = [];
40
+ private decisions: ImportDecisions = {
41
+ strategy: 'merge',
42
+ resolutions: {},
43
+ pathMappings: [],
44
+ };
45
+ private secretInputs: Record<string, string> = {};
46
+
47
+ constructor(opts: ImportWizardOptions) {
48
+ this.port = opts.port;
49
+ this.onProgress = opts.onProgress;
50
+ this.tracker = new ProgressTracker(IMPORT_STAGES, this.onProgress);
51
+ this.rollbackOnError = opts.defaultRollbackOnError ?? true;
52
+ }
53
+
54
+ /** 当前状态快照(React 绑定 / 测试断言用) */
55
+ snapshot(): WizardSnapshot {
56
+ return {
57
+ step: this.step,
58
+ zipPath: this.zipPath,
59
+ analysis: this.analysis,
60
+ plan: this.plan,
61
+ result: this.result,
62
+ rollbackOnError: this.rollbackOnError,
63
+ errors: [...this.errors],
64
+ };
65
+ }
66
+
67
+ get currentStep(): ImportStep {
68
+ return this.step;
69
+ }
70
+
71
+ /** 步骤 1-2:选 ZIP → Analyzing → Compatibility(analyzeImport 零写入) */
72
+ async selectZip(path: string): Promise<ImportAnalysis> {
73
+ this.zipPath = path;
74
+ this.step = 'analyzing';
75
+ this.tracker.emit('validating');
76
+ this.errors = [];
77
+ try {
78
+ this.analysis = await this.port.analyzeImport(path);
79
+ this.tracker.emit('checking-compatibility');
80
+ if (!this.analysis.valid) {
81
+ // 分析失败(完整性/schema/兼容性):错误进 errors,UI 停在失败态
82
+ this.errors.push(...this.analysis.errors.map((e) => formatActionableError(toActionableError(new Error(e)))));
83
+ throw new Error(this.analysis.errors.join('; ') || '备份分析失败');
84
+ }
85
+ this.step = 'compatibility';
86
+ return this.analysis;
87
+ } catch (err) {
88
+ // 仅当尚未记录分析错误时才追加(避免重复)
89
+ if (this.errors.length === 0) {
90
+ this.errors.push(formatActionableError(toActionableError(err)));
91
+ }
92
+ throw err;
93
+ }
94
+ }
95
+
96
+ /** 步骤 3→4:用户确认兼容性后进入 Preview(Dry Run:用当前决策生成计划摘要,零写入) */
97
+ async confirmCompatibility(): Promise<ImportPlan> {
98
+ if (this.analysis === null || this.zipPath === null) {
99
+ throw new Error('尚未完成分析,请先选择备份文件');
100
+ }
101
+ this.plan = await this.port.createImportPlan(this.zipPath, this.decisions);
102
+ this.step = 'preview';
103
+ return this.plan;
104
+ }
105
+
106
+ /** 更新全局冲突策略(merge/replace/skipExisting,规范 §11) */
107
+ setStrategy(strategy: GlobalConflictStrategy): void {
108
+ this.decisions = { ...this.decisions, strategy };
109
+ }
110
+
111
+ /** 设置逐项冲突决策(keepCurrent/useImported/review) */
112
+ setResolutions(resolutions: Record<string, 'keepCurrent' | 'useImported' | 'review'>): void {
113
+ this.decisions = { ...this.decisions, resolutions };
114
+ }
115
+
116
+ /** 设置路径映射(§12) */
117
+ setPathMappings(mappings: PathMapping[]): void {
118
+ this.decisions = { ...this.decisions, pathMappings: mappings };
119
+ }
120
+
121
+ /** 设置秘密补录值(仅内存,绝不持久化) */
122
+ setSecretInputs(inputs: Record<string, string>): void {
123
+ this.secretInputs = inputs;
124
+ }
125
+
126
+ /** Preview 摘要(规范 §10 数值化;基于当前 plan 与 analysis) */
127
+ previewSummary(): ImportPreviewSummary {
128
+ const items = this.plan?.items ?? [];
129
+ const analysis = this.analysis;
130
+ const count = (kinds: PlanItem['kind'][]): number =>
131
+ items.filter((i) => kinds.includes(i.kind)).length;
132
+ return {
133
+ willChange: count(['Create', 'Update', 'Install', 'Conflict']),
134
+ unchanged: count(['Skip']),
135
+ settingsUpdates: count(['Create', 'Update']),
136
+ pluginsInstalled: analysis?.pluginSummary.installed ?? 0,
137
+ pluginsToInstall: count(['Install']),
138
+ mcpAdds: items.filter((i) => i.adapter === 'mcp' && i.kind === 'Create').length,
139
+ prompts: items.filter((i) => i.adapter === 'prompts' && i.kind !== 'Skip').length,
140
+ pathMappingsNeeded: analysis?.pathIssues.length ?? 0,
141
+ secretsNeeded: this.plan?.missingSecrets.length ?? analysis?.secretCount ?? 0,
142
+ conflicts: count(['Conflict']),
143
+ needsRestart: this.plan?.needsRestart ?? false,
144
+ };
145
+ }
146
+
147
+ /** 冲突项(供 ConflictCollector 使用) */
148
+ conflictItems(): PlanItem[] {
149
+ return (this.plan?.items ?? []).filter((i) => i.kind === 'Conflict');
150
+ }
151
+
152
+ /** 设置回滚策略(场景 E 选择;执行前调用) */
153
+ setRollbackOnError(enable: boolean): void {
154
+ this.rollbackOnError = enable;
155
+ }
156
+
157
+ /**
158
+ * 步骤 10-14:确认导入 → 快照 → 执行 → 校验 → 结果。
159
+ * 用最终决策重建计划(与预览一致),显式传 rollbackOnError。
160
+ */
161
+ async execute(opts: { confirm: boolean; rollbackOnError?: boolean }): Promise<ImportResult> {
162
+ if (this.zipPath === null || this.analysis === null) {
163
+ throw new Error('尚未完成分析,请先选择备份文件');
164
+ }
165
+ if (opts.confirm !== true) {
166
+ throw new Error('导入未确认:必须确认后才允许修改任何数据');
167
+ }
168
+ const rollbackOnError = opts.rollbackOnError ?? this.rollbackOnError;
169
+ this.step = 'importing';
170
+ this.tracker.emit('creating-snapshot');
171
+
172
+ try {
173
+ // 用最终决策重建计划(与预览逻辑一致,保证 Dry Run 与真实导入一致)
174
+ this.plan = await this.port.createImportPlan(this.zipPath, this.decisions);
175
+ this.tracker.emit('restoring-settings');
176
+ this.tracker.emit('restoring-plugins');
177
+ this.tracker.emit('restoring-mcp');
178
+ this.tracker.emit('validating-config');
179
+ this.result = await this.port.executeImportPlan(this.zipPath, this.plan, {
180
+ confirm: true,
181
+ secretInputs: this.secretInputs,
182
+ rollbackOnError,
183
+ });
184
+ // 失败且已整体回滚(场景 E)→ 报告回滚阶段
185
+ if (!this.result.ok && this.result.rollback) {
186
+ this.tracker.emit('rolling-back');
187
+ }
188
+ this.tracker.emit('done');
189
+ this.step = 'result';
190
+ return this.result;
191
+ } catch (err) {
192
+ this.errors.push(formatActionableError(toActionableError(err)));
193
+ throw err;
194
+ }
195
+ }
196
+
197
+ /** 重置向导(可复用实例开始新导入) */
198
+ reset(): void {
199
+ this.step = 'select';
200
+ this.zipPath = null;
201
+ this.analysis = null;
202
+ this.plan = null;
203
+ this.result = null;
204
+ this.errors = [];
205
+ this.decisions = { strategy: 'merge', resolutions: {}, pathMappings: [] };
206
+ this.secretInputs = {};
207
+ }
208
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * path-mapping 测试(m6-ui,规范 §12):
3
+ * 单项映射 + 批量前缀映射、preview 替换、toPathMappings 只含已解析、isComplete。
4
+ */
5
+ import test from 'node:test';
6
+ import assert from 'node:assert/strict';
7
+ import { PathMappingEditor } from './path-mapping.ts';
8
+ import type { PathIssue } from '../core/types.ts';
9
+
10
+ const ISSUES: PathIssue[] = [
11
+ { kind: 'missing', value: 'C:\\Users\\alice\\projects\\ops' },
12
+ { kind: 'platformMismatch', value: 'C:\\Users\\alice\\projects\\dev' },
13
+ { kind: 'missing', value: '/Users/alice/work' },
14
+ ];
15
+
16
+ test('path-mapping: 从 issues 初始化草稿,全部未解析', () => {
17
+ const ed = new PathMappingEditor({ issues: ISSUES });
18
+ assert.equal(ed.all.length, 3);
19
+ assert.equal(ed.unresolved.length, 3);
20
+ assert.equal(ed.isComplete, false);
21
+ assert.equal(ed.toPathMappings().length, 0);
22
+ });
23
+
24
+ test('path-mapping: 单项映射后 toPathMappings 只含已解析', () => {
25
+ const ed = new PathMappingEditor({ issues: ISSUES });
26
+ ed.setMapping('C:\\Users\\alice\\projects\\ops', 'D:\\data\\ops');
27
+ const mappings = ed.toPathMappings();
28
+ assert.equal(mappings.length, 1);
29
+ assert.equal(mappings[0]!.oldPrefix, 'C:\\Users\\alice\\projects\\ops');
30
+ assert.equal(mappings[0]!.newPrefix, 'D:\\data\\ops');
31
+ assert.deepEqual(mappings[0]!.appliesTo, []);
32
+ assert.equal(ed.unresolved.length, 2);
33
+ assert.equal(ed.isComplete, false);
34
+ });
35
+
36
+ test('path-mapping: 批量前缀映射聚合到单条 draft', () => {
37
+ const ed = new PathMappingEditor({ issues: ISSUES });
38
+ ed.setMapping('C:\\Users\\alice\\', 'C:\\Users\\bob\\');
39
+ const mappings = ed.toPathMappings();
40
+ assert.equal(mappings.length, 1);
41
+ assert.equal(mappings[0]!.oldPrefix, 'C:\\Users\\alice\\');
42
+ // 三条 issue 中两条以该前缀开头 → 全部视为已解决;/Users/alice/work 未解决
43
+ assert.equal(ed.unresolved.length, 1);
44
+ });
45
+
46
+ test('path-mapping: preview 用 applyPrefixMappings 替换', () => {
47
+ const ed = new PathMappingEditor({ issues: ISSUES });
48
+ ed.setMapping('C:\\Users\\alice\\', 'C:\\Users\\bob\\');
49
+ assert.equal(ed.preview('C:\\Users\\alice\\projects\\ops'), 'C:/Users/bob/projects/ops');
50
+ assert.equal(ed.preview('/Users/alice/work'), '/Users/alice/work');
51
+ });
52
+
53
+ test('path-mapping: 全部解决后 isComplete 为 true', () => {
54
+ const ed = new PathMappingEditor({ issues: ISSUES });
55
+ ed.setMapping('C:\\Users\\alice\\', 'C:\\Users\\bob\\');
56
+ ed.setMapping('/Users/alice/work', '/home/bob/work');
57
+ assert.equal(ed.isComplete, true);
58
+ assert.equal(ed.toPathMappings().length, 2);
59
+ });
60
+
61
+ test('path-mapping: existing 预置映射可直接复用(oldPrefix 聚合)', () => {
62
+ const ed = new PathMappingEditor({
63
+ existing: [{ oldPrefix: 'C:\\Users\\alice\\', newPrefix: 'C:\\Users\\bob\\', appliesTo: ['workspaces', 'mcp'] }],
64
+ });
65
+ assert.equal(ed.resolved.length, 1);
66
+ assert.deepEqual(ed.resolved[0]!.appliesTo, ['workspaces', 'mcp']);
67
+ });
@@ -0,0 +1,103 @@
1
+ /**
2
+ * 路径映射编辑器(规范 §12,m6-ui)。
3
+ *
4
+ * 两种形态(§12 批量映射优先):
5
+ * - 单项映射:某条具体 PathIssue(如 workspace 绝对路径)→ 新路径;
6
+ * - 批量前缀映射:oldPrefix → newPrefix,一次替换全部相关路径。
7
+ *
8
+ * 输出 core PathMapping[]:newPrefix 非空的映射才进入执行计划
9
+ * (core mergePathMappings 只保留已解析映射;未解析项保留在 plan.items 供 UI 提示)。
10
+ * 应用范围 appliesTo 缺省 [](全部相关分区),与 core applyMappingsToSections 语义一致。
11
+ */
12
+ import type { PathIssue, PathMapping } from '../core/types.ts';
13
+ import { applyPrefixMappings } from '../utils/paths.ts';
14
+ import type { PathMappingAppliesTo, PathMappingDraft } from './types.ts';
15
+
16
+ export interface PathMappingEditorOptions {
17
+ /** 从分析结果得到的路径问题 */
18
+ issues?: PathIssue[];
19
+ /** 预置映射(如上次导入可复用的映射) */
20
+ existing?: PathMapping[];
21
+ }
22
+
23
+ export class PathMappingEditor {
24
+ private readonly issues: PathIssue[];
25
+ private drafts = new Map<string, PathMappingDraft>();
26
+
27
+ constructor(opts: PathMappingEditorOptions = {}) {
28
+ this.issues = opts.issues ?? [];
29
+ for (const issue of this.issues) {
30
+ this.drafts.set(issue.value, {
31
+ oldPrefix: issue.value,
32
+ newPrefix: issue.mappedTo ?? '',
33
+ issue,
34
+ appliesTo: [],
35
+ });
36
+ }
37
+ // 批量映射按 oldPrefix 聚合(一个 oldPrefix 一条 draft)
38
+ for (const m of opts.existing ?? []) {
39
+ const prev = this.drafts.get(m.oldPrefix);
40
+ this.drafts.set(m.oldPrefix, {
41
+ oldPrefix: m.oldPrefix,
42
+ newPrefix: m.newPrefix,
43
+ appliesTo: [...m.appliesTo] as PathMappingAppliesTo[],
44
+ });
45
+ void prev;
46
+ }
47
+ }
48
+
49
+ /** 全部草稿(含未解析) */
50
+ get all(): PathMappingDraft[] {
51
+ return [...this.drafts.values()];
52
+ }
53
+
54
+ /** 已解析(newPrefix 非空)的草稿 */
55
+ get resolved(): PathMappingDraft[] {
56
+ return this.all.filter((d) => d.newPrefix !== '');
57
+ }
58
+
59
+ /** 未解析的问题(UI 需提示用户选择文件夹;按「是否被已解析映射覆盖」判断,批量前缀映射一次覆盖多条) */
60
+ get unresolved(): PathIssue[] {
61
+ const mappings = this.toPathMappings();
62
+ return (this.issues ?? []).filter((issue) => {
63
+ const mapped = applyPrefixMappings(issue.value, mappings);
64
+ return mapped === issue.value;
65
+ });
66
+ }
67
+
68
+ /** 单项/批量设置映射(oldPrefix 已存在则更新;否则新建批量映射) */
69
+ setMapping(oldPrefix: string, newPrefix: string, appliesTo: PathMappingAppliesTo[] = []): void {
70
+ const existing = this.drafts.get(oldPrefix);
71
+ this.drafts.set(oldPrefix, {
72
+ oldPrefix,
73
+ newPrefix,
74
+ appliesTo,
75
+ issue: existing?.issue,
76
+ });
77
+ }
78
+
79
+ /** 对某个具体值应用当前映射,返回替换结果(预览用;无映射则原样返回) */
80
+ preview(value: string): string {
81
+ const resolved = this.resolved.map((d) => ({
82
+ oldPrefix: d.oldPrefix,
83
+ newPrefix: d.newPrefix,
84
+ appliesTo: d.appliesTo,
85
+ }));
86
+ const mapped = applyPrefixMappings(value, resolved);
87
+ return typeof mapped === 'string' ? mapped : value;
88
+ }
89
+
90
+ /** 未解析项是否可进入执行(false = 有路径问题未解决) */
91
+ get isComplete(): boolean {
92
+ return this.unresolved.length === 0;
93
+ }
94
+
95
+ /** 输出 core PathMapping[](仅已解析项) */
96
+ toPathMappings(): PathMapping[] {
97
+ return this.resolved.map((d) => ({
98
+ oldPrefix: d.oldPrefix,
99
+ newPrefix: d.newPrefix,
100
+ appliesTo: d.appliesTo as PathMapping['appliesTo'],
101
+ }));
102
+ }
103
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * progress 测试(m6-ui,规范 §29):阶段文案与进度追踪。
3
+ */
4
+ import test from 'node:test';
5
+ import assert from 'node:assert/strict';
6
+ import { EXPORT_STAGES, IMPORT_STAGES, ProgressTracker, stageText } from './progress.ts';
7
+
8
+ test('progress: 阶段文案映射(导出/导入/回滚)', () => {
9
+ assert.equal(stageText('analyzing'), 'Analyzing configuration...');
10
+ assert.equal(stageText('creating-snapshot'), 'Creating safety snapshot...');
11
+ assert.equal(stageText('rolling-back'), 'Rolling back...');
12
+ assert.equal(stageText('restoring-mcp'), 'Restoring MCP...');
13
+ assert.equal(stageText('done'), 'Done');
14
+ // 未知阶段回退 id
15
+ assert.equal(stageText('unknown-phase'), 'unknown-phase');
16
+ });
17
+
18
+ test('progress: 导出阶段序列完整', () => {
19
+ assert.deepEqual(EXPORT_STAGES, [
20
+ 'analyzing', 'exporting-settings', 'scanning-secrets', 'exporting-plugins',
21
+ 'creating-archive', 'calculating-checksums', 'done',
22
+ ]);
23
+ });
24
+
25
+ test('progress: 导入阶段序列含快照/回滚', () => {
26
+ assert.ok(IMPORT_STAGES.includes('creating-snapshot'));
27
+ assert.ok(IMPORT_STAGES.includes('rolling-back'));
28
+ assert.ok(IMPORT_STAGES.includes('restoring-settings'));
29
+ });
30
+
31
+ test('progress: tracker 发出事件并带 step/total', () => {
32
+ const events: { stage: string; step?: number; total?: number }[] = [];
33
+ const t = new ProgressTracker(IMPORT_STAGES, (e) => events.push(e));
34
+ t.emit('validating');
35
+ t.emit('creating-snapshot');
36
+ t.emit('done');
37
+ assert.deepEqual(t.events, ['validating', 'creating-snapshot', 'done']);
38
+ assert.equal(events[0]!.step, 1);
39
+ assert.equal(events[0]!.total, IMPORT_STAGES.length);
40
+ assert.equal(events[1]!.step, 3);
41
+ assert.equal(events[2]!.step, IMPORT_STAGES.length);
42
+ });
@@ -0,0 +1,90 @@
1
+ /**
2
+ * 进度阶段文案与进度追踪(规范 §29,m6-ui)。
3
+ *
4
+ * 纯文本层:每个阶段 id 有默认文案;未知阶段回退显示 id 本身。
5
+ * ProgressTracker 供导出/导入控制器在调用 core 前后发出阶段事件(UI 不冻结)。
6
+ */
7
+ import type { ProgressEvent, ProgressListener } from './types.ts';
8
+
9
+ /** 导出阶段文案(规范 §29 Export 示例) */
10
+ export const EXPORT_STAGES: readonly string[] = [
11
+ 'analyzing', // Analyzing configuration...
12
+ 'exporting-settings', // Exporting settings...
13
+ 'scanning-secrets', // Scanning secrets...
14
+ 'exporting-plugins', // Exporting plugins...
15
+ 'creating-archive', // Creating archive...
16
+ 'calculating-checksums', // Calculating checksums...
17
+ 'done',
18
+ ] as const;
19
+
20
+ /** 导入阶段文案(规范 §29 Import 示例 + 快照/回滚阶段) */
21
+ export const IMPORT_STAGES: readonly string[] = [
22
+ 'validating', // Validating backup...
23
+ 'checking-compatibility', // Checking compatibility...
24
+ 'creating-snapshot', // Creating safety snapshot...
25
+ 'restoring-settings', // Restoring settings...
26
+ 'restoring-plugins', // Restoring plugins...
27
+ 'restoring-mcp', // Restoring MCP...
28
+ 'validating-config', // Validating configuration...
29
+ 'rolling-back', // Rolling back...(仅失败回滚时)
30
+ 'done',
31
+ ] as const;
32
+
33
+ const STAGE_TEXTS: Record<string, string> = {
34
+ // export
35
+ 'analyzing': 'Analyzing configuration...',
36
+ 'exporting-settings': 'Exporting settings...',
37
+ 'scanning-secrets': 'Scanning secrets...',
38
+ 'exporting-plugins': 'Exporting plugins...',
39
+ 'creating-archive': 'Creating archive...',
40
+ 'calculating-checksums': 'Calculating checksums...',
41
+ // import
42
+ 'validating': 'Validating backup...',
43
+ 'checking-compatibility': 'Checking compatibility...',
44
+ 'creating-snapshot': 'Creating safety snapshot...',
45
+ 'restoring-settings': 'Restoring settings...',
46
+ 'restoring-plugins': 'Restoring plugins...',
47
+ 'restoring-mcp': 'Restoring MCP...',
48
+ 'validating-config': 'Validating configuration...',
49
+ 'rolling-back': 'Rolling back...',
50
+ // common
51
+ 'done': 'Done',
52
+ };
53
+
54
+ /** 阶段 id → 用户可读文案(未知阶段回退 id) */
55
+ export function stageText(stage: string): string {
56
+ return STAGE_TEXTS[stage] ?? stage;
57
+ }
58
+
59
+ /**
60
+ * 进度追踪器:按给定阶段序列在回调时附带 step/total 序号。
61
+ * 控制器调用 core 前后 emit(),UI 侧渲染进度条/阶段文字。
62
+ */
63
+ export class ProgressTracker {
64
+ private readonly listener: ProgressListener | undefined;
65
+ private readonly stages: readonly string[];
66
+ private readonly emitted: string[] = [];
67
+
68
+ constructor(stages: readonly string[], listener?: ProgressListener) {
69
+ this.stages = stages;
70
+ this.listener = listener;
71
+ }
72
+
73
+ /** 发出某阶段事件(可携带 detail);未在序列中的阶段 step/total 缺省 */
74
+ emit(stage: string, detail?: string): void {
75
+ const idx = this.stages.indexOf(stage);
76
+ const event: ProgressEvent = {
77
+ stage,
78
+ detail,
79
+ step: idx >= 0 ? idx + 1 : undefined,
80
+ total: idx >= 0 ? this.stages.length : undefined,
81
+ };
82
+ this.emitted.push(stage);
83
+ this.listener?.(event);
84
+ }
85
+
86
+ /** 已发出阶段的快照(测试与日志用) */
87
+ get events(): readonly string[] {
88
+ return [...this.emitted];
89
+ }
90
+ }