dsh-config-manager 0.1.78 → 0.1.79

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 (290) hide show
  1. package/lib/adapters/credentials.d.ts +6 -2
  2. package/lib/adapters/credentials.js +19 -3
  3. package/lib/adapters/file-collection.js +2 -1
  4. package/lib/adapters/mcp.d.ts +48 -2
  5. package/lib/adapters/mcp.js +265 -3
  6. package/lib/adapters/messages.d.ts +3 -0
  7. package/lib/adapters/messages.js +7 -0
  8. package/lib/adapters/plugin-files.js +2 -1
  9. package/lib/adapters/plugins.js +105 -20
  10. package/lib/adapters/pnpm-workspace.d.ts +32 -0
  11. package/lib/adapters/pnpm-workspace.js +113 -2
  12. package/lib/adapters/prompts.d.ts +6 -2
  13. package/lib/adapters/prompts.js +17 -2
  14. package/lib/adapters/providers.d.ts +19 -2
  15. package/lib/adapters/providers.js +59 -3
  16. package/lib/adapters/settings.d.ts +6 -2
  17. package/lib/adapters/settings.js +13 -2
  18. package/lib/adapters/skills.d.ts +25 -1
  19. package/lib/adapters/skills.js +60 -0
  20. package/lib/adapters/ui.d.ts +6 -2
  21. package/lib/adapters/ui.js +13 -2
  22. package/lib/adapters/units.d.ts +18 -0
  23. package/lib/adapters/units.js +39 -0
  24. package/lib/adapters/workspaces.js +2 -1
  25. package/lib/client.d.ts +400 -112
  26. package/lib/client.js +2845 -1524
  27. package/lib/core/backup.js +20 -0
  28. package/lib/core/mcp-connector-store.d.ts +159 -0
  29. package/lib/core/mcp-connector-store.js +445 -0
  30. package/lib/core/messages-core.d.ts +2 -0
  31. package/lib/core/messages-core.js +4 -0
  32. package/lib/core/messages.d.ts +20 -0
  33. package/lib/core/messages.js +44 -0
  34. package/lib/core/phase3-host.js +18 -1
  35. package/lib/core/plugin-cli.d.ts +22 -2
  36. package/lib/core/plugin-cli.js +22 -3
  37. package/lib/core/recovery-orchestrator.d.ts +3 -1
  38. package/lib/core/recovery-orchestrator.js +75 -16
  39. package/lib/core/rollback-activity.d.ts +89 -0
  40. package/lib/core/rollback-activity.js +350 -0
  41. package/lib/core/rollback-ledger.d.ts +0 -9
  42. package/lib/core/rollback-ledger.js +37 -0
  43. package/lib/core/rollback-merge.d.ts +22 -2
  44. package/lib/core/rollback-merge.js +63 -13
  45. package/lib/core/rollback-point.d.ts +93 -0
  46. package/lib/core/rollback-point.js +420 -101
  47. package/lib/core/rollback-undo-journal.d.ts +123 -0
  48. package/lib/core/rollback-undo-journal.js +517 -0
  49. package/lib/core/self-update.js +2 -4
  50. package/lib/core/text-fidelity.d.ts +89 -0
  51. package/lib/core/text-fidelity.js +176 -0
  52. package/lib/core/types.d.ts +12 -3
  53. package/lib/core/update-check.js +2 -4
  54. package/lib/core/validator.d.ts +9 -0
  55. package/lib/core/validator.js +12 -0
  56. package/lib/foreign/bundle.js +42 -3
  57. package/lib/foreign/claude-code.js +358 -26
  58. package/lib/foreign/claude-sessions.d.ts +51 -8
  59. package/lib/foreign/claude-sessions.js +111 -13
  60. package/lib/foreign/read-claude-code.d.ts +33 -4
  61. package/lib/foreign/read-claude-code.js +446 -65
  62. package/lib/foreign/read-claude-plugins.d.ts +36 -0
  63. package/lib/foreign/read-claude-plugins.js +403 -0
  64. package/lib/foreign/registry.js +25 -2
  65. package/lib/foreign/truth-table.js +6 -4
  66. package/lib/foreign/types.d.ts +170 -2
  67. package/lib/index.d.ts +14 -5
  68. package/lib/index.js +60 -16
  69. package/lib/market/git-file-writer.js +7 -1
  70. package/lib/profiles/dsh-profile-launcher.d.ts +20 -0
  71. package/lib/profiles/dsh-profile-launcher.js +58 -1
  72. package/lib/profiles/dsh-profile-shared.d.ts +8 -2
  73. package/lib/profiles/dsh-profile-shared.js +8 -2
  74. package/lib/routes/foreign.js +8 -1
  75. package/lib/routes/index.d.ts +1 -1
  76. package/lib/routes/kit.d.ts +2 -2
  77. package/lib/routes/sync.js +173 -9
  78. package/lib/schema/manifest.d.ts +6 -1
  79. package/lib/schema/manifest.js +55 -0
  80. package/lib/schema/types.d.ts +47 -2
  81. package/lib/sync/autosync-config.d.ts +73 -0
  82. package/lib/sync/autosync-config.js +89 -0
  83. package/lib/sync/autosync-scheduler.d.ts +80 -6
  84. package/lib/sync/autosync-scheduler.js +263 -40
  85. package/lib/sync/gist/gist-transport.d.ts +15 -1
  86. package/lib/sync/gist/gist-transport.js +66 -5
  87. package/lib/sync/gist/messages.d.ts +3 -0
  88. package/lib/sync/gist/messages.js +7 -0
  89. package/lib/sync/git/git-transport.d.ts +42 -3
  90. package/lib/sync/git/git-transport.js +146 -3
  91. package/lib/sync/merge.d.ts +47 -14
  92. package/lib/sync/merge.js +242 -222
  93. package/lib/sync/messages.d.ts +8 -0
  94. package/lib/sync/messages.js +19 -0
  95. package/lib/sync/risk.d.ts +19 -2
  96. package/lib/sync/risk.js +10 -3
  97. package/lib/sync/s3/messages.d.ts +6 -0
  98. package/lib/sync/s3/messages.js +13 -0
  99. package/lib/sync/s3/s3-transport.d.ts +9 -1
  100. package/lib/sync/s3/s3-transport.js +78 -6
  101. package/lib/sync/sync-engine.d.ts +193 -4
  102. package/lib/sync/sync-engine.js +450 -37
  103. package/lib/sync/sync-history.d.ts +11 -0
  104. package/lib/sync/sync-history.js +20 -0
  105. package/lib/sync/sync-state.d.ts +43 -0
  106. package/lib/sync/sync-state.js +44 -0
  107. package/lib/sync/sync-watcher.d.ts +76 -0
  108. package/lib/sync/sync-watcher.js +202 -0
  109. package/lib/sync/text-merge.d.ts +85 -0
  110. package/lib/sync/text-merge.js +513 -0
  111. package/lib/sync/transport.d.ts +48 -0
  112. package/lib/sync/transport.js +12 -0
  113. package/lib/sync/watch-scope.d.ts +38 -0
  114. package/lib/sync/watch-scope.js +95 -0
  115. package/lib/sync/webdav/webdav-transport.d.ts +16 -2
  116. package/lib/sync/webdav/webdav-transport.js +128 -11
  117. package/lib/ui/foreign-view.js +10 -0
  118. package/lib/ui/i18n.d.ts +8 -0
  119. package/lib/ui/i18n.js +20 -0
  120. package/lib/ui/rollback-view.d.ts +12 -0
  121. package/lib/ui/rollback-view.js +2 -0
  122. package/lib/ui/sync-settings-view.d.ts +47 -0
  123. package/lib/ui/sync-settings-view.js +24 -0
  124. package/lib/ui/types.d.ts +21 -0
  125. package/lib/utils/env-lock.d.ts +1 -0
  126. package/lib/utils/env-lock.js +7 -4
  127. package/package.json +6 -6
  128. package/src/adapters/credentials.ts +22 -4
  129. package/src/adapters/file-collection.ts +2 -1
  130. package/src/adapters/mcp.test.ts +235 -1
  131. package/src/adapters/mcp.ts +276 -5
  132. package/src/adapters/messages.ts +7 -0
  133. package/src/adapters/plugin-files.ts +2 -1
  134. package/src/adapters/plugins.test.ts +176 -2
  135. package/src/adapters/plugins.ts +107 -21
  136. package/src/adapters/pnpm-workspace.test.ts +89 -1
  137. package/src/adapters/pnpm-workspace.ts +127 -2
  138. package/src/adapters/prompts.ts +20 -3
  139. package/src/adapters/providers.ts +62 -4
  140. package/src/adapters/settings.ts +16 -3
  141. package/src/adapters/skills.test.ts +141 -0
  142. package/src/adapters/skills.ts +58 -2
  143. package/src/adapters/ui.ts +16 -3
  144. package/src/adapters/units-entries.test.ts +190 -0
  145. package/src/adapters/units.ts +50 -0
  146. package/src/adapters/workspaces.ts +2 -1
  147. package/src/client/about/PluginUpdateDialog.tsx +4 -2
  148. package/src/client/about/ReleaseNotesDialog.tsx +4 -2
  149. package/src/client/chat/RollbackReviewDialog.tsx +3 -2
  150. package/src/client/common/Skeleton.tsx +32 -0
  151. package/src/client/common/routes.ts +9 -0
  152. package/src/client/config-manager.module.css +321 -11
  153. package/src/client/home/HomePanel.tsx +14 -6
  154. package/src/client/import/ForeignImportView.tsx +3 -1
  155. package/src/client/import/import-wizard-steps.tsx +2 -1
  156. package/src/client/library/ArtifactRow.tsx +12 -2
  157. package/src/client/library/LibraryActions.tsx +6 -3
  158. package/src/client/library/LibraryPanel.tsx +21 -3
  159. package/src/client/library/RestorePlanView.tsx +4 -2
  160. package/src/client/locales.ts +3 -0
  161. package/src/client/recovery/RecoveryPanel.tsx +54 -7
  162. package/src/client/recovery/RollbackCard.tsx +7 -2
  163. package/src/client/recovery/RollbackDiffPane.tsx +3 -2
  164. package/src/client/recovery/recovery-locales.ts +20 -2
  165. package/src/client/recovery/recovery-view.test.ts +39 -1
  166. package/src/client/recovery/recovery-view.ts +23 -1
  167. package/src/client/run-store.test.ts +1 -1
  168. package/src/client/run-store.ts +4 -0
  169. package/src/client/sync/AutosyncCard.tsx +49 -6
  170. package/src/client/sync/MergeModal.tsx +219 -0
  171. package/src/client/sync/MergePreviewView.tsx +306 -0
  172. package/src/client/sync/SyncActivityBar.tsx +31 -0
  173. package/src/client/sync/SyncConfirmView.tsx +2 -1
  174. package/src/client/sync/SyncPanel.tsx +130 -87
  175. package/src/client/sync/SyncProbeRow.tsx +162 -0
  176. package/src/client/sync/SyncSettingsView.test.ts +27 -2
  177. package/src/client/sync/WorkDirCard.tsx +44 -6
  178. package/src/client/sync/history-model.ts +4 -1
  179. package/src/client/sync/merge-view.test.ts +234 -0
  180. package/src/client/sync/merge-view.ts +371 -0
  181. package/src/client/sync/probe-view.test.ts +90 -0
  182. package/src/client/sync/probe-view.ts +63 -0
  183. package/src/client/sync/remote-device-view.test.ts +35 -0
  184. package/src/client/sync/remote-device-view.ts +25 -0
  185. package/src/client/sync/sync-api.ts +212 -2
  186. package/src/client/sync/sync-entry.test.ts +78 -0
  187. package/src/client/sync/sync-entry.ts +53 -0
  188. package/src/client/sync/sync-locales.ts +234 -2
  189. package/src/client/sync/sync-view.test.ts +18 -1
  190. package/src/client/sync/sync-view.ts +20 -0
  191. package/src/core/backup.test.ts +41 -0
  192. package/src/core/backup.ts +20 -0
  193. package/src/core/mcp-connector-store.test.ts +265 -0
  194. package/src/core/mcp-connector-store.ts +516 -0
  195. package/src/core/messages-core.ts +4 -0
  196. package/src/core/messages.ts +46 -0
  197. package/src/core/phase3-host.ts +18 -1
  198. package/src/core/phase3-production-integration.test.ts +4 -0
  199. package/src/core/plugin-cli.test.ts +26 -0
  200. package/src/core/plugin-cli.ts +30 -4
  201. package/src/core/recovery-orchestrator.test.ts +104 -5
  202. package/src/core/recovery-orchestrator.ts +74 -17
  203. package/src/core/rollback-activity.test.ts +311 -0
  204. package/src/core/rollback-activity.ts +367 -0
  205. package/src/core/rollback-cross-session.test.ts +694 -0
  206. package/src/core/rollback-interrupted.test.ts +137 -0
  207. package/src/core/rollback-ledger.ts +32 -0
  208. package/src/core/rollback-merge.test.ts +130 -0
  209. package/src/core/rollback-merge.ts +78 -14
  210. package/src/core/rollback-point.ts +468 -91
  211. package/src/core/rollback-undo-journal.test.ts +508 -0
  212. package/src/core/rollback-undo-journal.ts +571 -0
  213. package/src/core/self-update.ts +2 -5
  214. package/src/core/text-fidelity.test.ts +189 -0
  215. package/src/core/text-fidelity.ts +197 -0
  216. package/src/core/types.ts +11 -3
  217. package/src/core/update-check.ts +2 -5
  218. package/src/core/validator.ts +14 -0
  219. package/src/foreign/bundle-origin.test.ts +265 -0
  220. package/src/foreign/bundle.ts +45 -5
  221. package/src/foreign/claude-code.test.ts +268 -0
  222. package/src/foreign/claude-code.ts +406 -25
  223. package/src/foreign/claude-plugins.test.ts +234 -0
  224. package/src/foreign/claude-sessions.test.ts +141 -0
  225. package/src/foreign/claude-sessions.ts +179 -23
  226. package/src/foreign/file-budget.test.ts +4 -1
  227. package/src/foreign/read-claude-code.test.ts +320 -0
  228. package/src/foreign/read-claude-code.ts +490 -71
  229. package/src/foreign/read-claude-plugins.ts +465 -0
  230. package/src/foreign/registry.ts +25 -2
  231. package/src/foreign/session-ir.test.ts +65 -0
  232. package/src/foreign/truth-table.test.ts +81 -0
  233. package/src/foreign/truth-table.ts +6 -3
  234. package/src/foreign/types.ts +162 -2
  235. package/src/index.facade.test.ts +26 -4
  236. package/src/index.ts +76 -19
  237. package/src/market/git-file-writer.test.ts +4 -0
  238. package/src/market/git-file-writer.ts +7 -1
  239. package/src/profiles/dsh-profile-launcher.test.ts +68 -3
  240. package/src/profiles/dsh-profile-launcher.ts +56 -1
  241. package/src/profiles/dsh-profile-shared.ts +8 -2
  242. package/src/routes/foreign.ts +8 -1
  243. package/src/routes/index.ts +1 -1
  244. package/src/routes/kit.ts +2 -2
  245. package/src/routes/sync.ts +176 -9
  246. package/src/schema/manifest-schema.test.ts +87 -2
  247. package/src/schema/manifest.ts +57 -1
  248. package/src/schema/types.ts +45 -2
  249. package/src/sync/autosync-config.test.ts +73 -1
  250. package/src/sync/autosync-config.ts +124 -0
  251. package/src/sync/autosync-risk-tier.test.ts +38 -2
  252. package/src/sync/autosync-scheduler.test.ts +264 -12
  253. package/src/sync/autosync-scheduler.ts +296 -44
  254. package/src/sync/gist/gist-conditional-write.test.ts +223 -0
  255. package/src/sync/gist/gist-transport.ts +73 -7
  256. package/src/sync/gist/messages.ts +7 -0
  257. package/src/sync/git/git-probe.test.ts +80 -0
  258. package/src/sync/git/git-transport.test.ts +185 -0
  259. package/src/sync/git/git-transport.ts +178 -5
  260. package/src/sync/merge.test.ts +188 -55
  261. package/src/sync/merge.ts +282 -224
  262. package/src/sync/messages.ts +19 -0
  263. package/src/sync/risk.ts +29 -5
  264. package/src/sync/s3/messages.ts +13 -0
  265. package/src/sync/s3/s3-conditional-write.test.ts +330 -0
  266. package/src/sync/s3/s3-transport.ts +87 -9
  267. package/src/sync/sync-device-identity.test.ts +267 -0
  268. package/src/sync/sync-engine.test.ts +379 -0
  269. package/src/sync/sync-engine.ts +535 -36
  270. package/src/sync/sync-history.test.ts +16 -1
  271. package/src/sync/sync-history.ts +26 -0
  272. package/src/sync/sync-state.test.ts +64 -0
  273. package/src/sync/sync-state.ts +79 -0
  274. package/src/sync/sync-watcher.test.ts +197 -0
  275. package/src/sync/sync-watcher.ts +246 -0
  276. package/src/sync/text-merge.test.ts +358 -0
  277. package/src/sync/text-merge.ts +554 -0
  278. package/src/sync/transport.ts +57 -0
  279. package/src/sync/watch-scope.test.ts +122 -0
  280. package/src/sync/watch-scope.ts +120 -0
  281. package/src/sync/webdav/webdav-conditional-write.test.ts +360 -0
  282. package/src/sync/webdav/webdav-transport.ts +137 -14
  283. package/src/ui/foreign-view.test.ts +34 -0
  284. package/src/ui/foreign-view.ts +10 -0
  285. package/src/ui/i18n.ts +20 -0
  286. package/src/ui/rollback-view.ts +14 -0
  287. package/src/ui/sync-settings-view.test.ts +28 -1
  288. package/src/ui/sync-settings-view.ts +43 -0
  289. package/src/ui/types.ts +22 -0
  290. package/src/utils/env-lock.ts +8 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-config-manager",
3
- "version": "0.1.78",
3
+ "version": "0.1.79",
4
4
  "license": "MIT",
5
5
  "description": "Backup, restore, export, import, migrate and sync your DeepSeek Harness (DSH) configuration: settings, plugins, MCP servers, skills, agent presets and workspaces. One-click migration to another machine. DSH 配置一键备份、恢复、导出、导入、迁移与同步插件。",
6
6
  "icon": "./icon.svg",
@@ -190,19 +190,19 @@
190
190
  "@deepseek-ai/dsh-timeout": "^0.1.0-rc.8",
191
191
  "@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
192
192
  "@deepseek-ai/dsh-workspace": "^0.1.0-rc.6",
193
- "@radix-ui/react-dialog": "^1.1.23",
193
+ "@radix-ui/react-dialog": "^1.3.1",
194
194
  "@types/js-yaml": "^4.0.9",
195
- "@types/node": "^26.2.0",
195
+ "@types/node": "^26.6.5",
196
196
  "@types/react": "~18.3.1",
197
197
  "@types/react-dom": "^18.3.5",
198
198
  "lightningcss": "1.33.0",
199
- "lucide": "^1.47.0",
200
- "lucide-react": "^1.41.0",
199
+ "lucide": "^1.61.0",
200
+ "lucide-react": "^1.61.0",
201
201
  "morphicons": "^1.7.1",
202
202
  "react": "^18.3.1",
203
203
  "react-dom": "^18.3.1",
204
204
  "schemastery": "^3.18.0",
205
- "tsdown": "0.23.0",
205
+ "tsdown": "0.23.1",
206
206
  "typescript": "^7.0.2"
207
207
  }
208
208
  }
@@ -11,12 +11,13 @@ import type { CredentialStatus, CredentialsSection } from '../schema/types.ts';
11
11
  import { msgOf, zhMsg } from '../core/messages.ts';
12
12
  import type { MsgFunc } from '../core/messages.ts';
13
13
  import type {
14
- ApplyResult, ConfigAdapter, ExportOptions, ExportSection, HostContext,
14
+ ApplyResult, ConfigAdapter, ExportOptions, ExportSection, ExportUnit, HostContext,
15
15
  ImportContext, PlanItem, ValidationResult,
16
16
  } from '../core/types.ts';
17
17
  import { namespaceFromMap, readNamespaceMap, resolveNamespaces, type NamespaceProvider } from './settings.ts';
18
18
  import { sectionMeta } from '../schema/section-registry.ts';
19
19
  import { validateJsonSection } from './json-section.ts';
20
+ import { entryByteSize, filterByUnits } from './units.ts';
20
21
 
21
22
  export type CredentialRefsProvider = (ctx: HostContext) => Promise<string[]>;
22
23
 
@@ -83,7 +84,7 @@ export class CredentialsAdapter implements ConfigAdapter<CredentialsSection> {
83
84
  this.refs = options.refs ?? defaultCredentialRefs(options.namespaces ?? []);
84
85
  }
85
86
 
86
- async export(ctx: HostContext, _options: ExportOptions): Promise<ExportSection<CredentialsSection>> {
87
+ async export(ctx: HostContext, options: ExportOptions): Promise<ExportSection<CredentialsSection>> {
87
88
  const credentials: CredentialStatus[] = [];
88
89
  const warnings: string[] = [];
89
90
  for (const ref of await this.refs(ctx)) {
@@ -100,14 +101,31 @@ export class CredentialsAdapter implements ConfigAdapter<CredentialsSection> {
100
101
  warnings.push(msgOf(ctx)('adapter.credStatusReadFailed', { ref, reason: err instanceof Error ? err.message : String(err) }));
101
102
  }
102
103
  }
104
+ // 条目级白名单:未勾选的凭据 ref 不进「需补录」清单(少带一条 = 少录一个值)
105
+ const picked = filterByUnits(credentials, options.includeItems?.[this.id], (c) => this.unitId(c.ref));
103
106
  return {
104
107
  sectionId: 'credentialsStatus',
105
- data: { version: 1, credentials },
106
- counts: { credentials: credentials.length },
108
+ data: { version: 1, credentials: picked },
109
+ counts: { credentials: picked.length },
107
110
  warnings,
108
111
  };
109
112
  }
110
113
 
114
+ /** 单元 id:`credentialsStatus:<凭据 ref>` */
115
+ private unitId(ref: string): string {
116
+ return this.id + ':' + ref;
117
+ }
118
+
119
+ /** 条目级单元清单(零 I/O);label = 凭据 ref,detail = 本机是否有值(不含任何敏感值) */
120
+ listUnits(section: ExportSection<CredentialsSection>): ExportUnit[] {
121
+ return (section.data.credentials ?? []).map((c) => ({
122
+ id: this.unitId(c.ref),
123
+ label: c.ref,
124
+ detail: c.configured ? 'configured' : 'missing',
125
+ sizeBytes: entryByteSize(c),
126
+ }));
127
+ }
128
+
111
129
  async analyzeImport(_data: CredentialsSection, _ctx: ImportContext): Promise<PlanItem[]> {
112
130
  // MissingSecret 计划项由引擎在 createImportPlan 兜底生成(analyzer.ensureMissingSecrets),
113
131
  // 依据 = 本分区里 configured=true 的凭据 → 用户补录清单。这里保持零写入纯计算。
@@ -324,7 +324,8 @@ export abstract class FileCollectionAdapter implements ConfigAdapter<FilesSectio
324
324
 
325
325
  /** 单元清单(零 I/O:直接由 export 产物归并)。 */
326
326
  listUnits(section: ExportSection<FilesSection>): ExportUnit[] {
327
- return unitsFromFiles(this.id, section.data.files, (rel) => this.unitIdOf(rel));
327
+ // files 可能缺省(空分区 / 手写 section):按空表处理,不抛错 —— 选择器对任何分区都会调 listUnits
328
+ return unitsFromFiles(this.id, section.data.files ?? [], (rel) => this.unitIdOf(rel));
328
329
  }
329
330
 
330
331
  /**
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import test from 'node:test';
6
6
  import assert from 'node:assert/strict';
7
- import { McpAdapter, extractMcpServers, buildMcpPatchLine } from './mcp.ts';
7
+ import { McpAdapter, extractMcpServers, buildMcpPatchLine, mcpServerUnitId, mcpConnectorUnitId } from './mcp.ts';
8
8
  import { makeContext, makeImportContext } from './test-helpers.ts';
9
9
  import type { PlanItem } from '../core/types.ts';
10
10
 
@@ -137,3 +137,237 @@ test('mcp: validate 拒绝无 serverName 的条目', async () => {
137
137
  const bad = await adapter.validate({ version: 1, servers: [{ serverName: '' } as never] });
138
138
  assert.equal(bad.valid, false);
139
139
  });
140
+
141
+ /* ------------------------------------------------------------ 连接器来源(方案 C) */
142
+
143
+ const enc = (s: string) => new TextEncoder().encode(s);
144
+
145
+ /** 造一份连接器存储文件(与真机同形态:{unit,global,tables:{connections,...}}) */
146
+ function seedStore(ctx: ReturnType<typeof makeContext>, connections: Record<string, unknown>): void {
147
+ void ctx.fs.writeFile('storages/mcp_connector.json', enc(JSON.stringify({
148
+ unit: { name: 'mcp_connector', version: 1 },
149
+ global: null,
150
+ tables: { connections, grants: { g1: { key: 'g1' } }, catalog: {}, snapshots: {} },
151
+ })));
152
+ }
153
+
154
+ const GH_TOKEN = 'ghp_' + 'A1b2C3d4E5f6G7h8I9j0K1l2M3n4O5p6Q7r8';
155
+
156
+ function githubConn(): Record<string, unknown> {
157
+ return {
158
+ key: 'github-github', connectorId: 'github', kind: 'manual', name: 'GitHub', serverKey: 'github', serverName: 'github',
159
+ transport: 'streamable-http', url: 'https://api.githubcopilot.com/mcp/',
160
+ headers: { Accept: 'application/json', Authorization: 'Bearer ' + GH_TOKEN },
161
+ auth: { mode: 'bearer', bearerToken: GH_TOKEN },
162
+ enabled: true, createdAt: 1, updatedAt: 2,
163
+ };
164
+ }
165
+
166
+ test('mcp 连接器:export 带上连接器并按规则脱敏(真机 4 条 MCP 的由来)', async () => {
167
+ const src = makeContext('win32', 'C:\\Users\\alice');
168
+ seedStore(src, { 'github-github': githubConn() });
169
+ const exported = await new McpAdapter().export(src, { includeSecrets: false });
170
+ assert.equal(exported.data.servers.length, 0, 'patch 层没有 server(真机形态)');
171
+ assert.equal(exported.data.connectors?.connections.length, 1, '连接器必须进备份,否则 MCP 分区恒为 0');
172
+ assert.equal(exported.counts['connectors'], 1);
173
+ const entry = exported.data.connectors?.connections[0];
174
+ assert.equal(entry?.serverName, 'github');
175
+ assert.equal(entry?.headers?.['Accept'], 'application/json');
176
+ assert.equal(entry?.headers?.['Authorization'], '<REDACTED:REENTER>');
177
+ assert.equal(JSON.stringify(exported.data).includes(GH_TOKEN), false, '备份里绝不能出现真实 token');
178
+ });
179
+
180
+ test('mcp 连接器:存储存在但解析不了 → 出 warning 且不当作空表(不静默)', async () => {
181
+ const src = makeContext('win32', 'C:\\Users\\alice');
182
+ void src.fs.writeFile('storages/mcp_connector.json', enc('{ broken'));
183
+ const exported = await new McpAdapter().export(src, { includeSecrets: false });
184
+ assert.equal(exported.data.connectors, undefined);
185
+ assert.equal(exported.warnings?.length, 1);
186
+ assert.match(String(exported.warnings?.[0]), /连接器存储/);
187
+ });
188
+
189
+ test('mcp 连接器:本机没有该文件 → 静默(没装连接器是正常形态)', async () => {
190
+ const src = makeContext('win32', 'C:\\Users\\alice');
191
+ const exported = await new McpAdapter().export(src, { includeSecrets: false });
192
+ assert.equal(exported.data.connectors, undefined);
193
+ assert.equal(exported.warnings?.length ?? 0, 0);
194
+ });
195
+
196
+ test('mcp 连接器:导入 Create → 写回 → 再分析 Skip(幂等),其它表原样保留', async () => {
197
+ const src = makeContext('win32', 'C:\\Users\\alice');
198
+ seedStore(src, { 'github-github': githubConn() });
199
+ const adapter = new McpAdapter();
200
+ const exported = await adapter.export(src, { includeSecrets: false });
201
+ const sections = new Map([['mcp', exported.data]]);
202
+
203
+ // 目标机已有存储(含 grants 与另一条无关连接)→ 写回时这些必须逐字保留
204
+ const dst = makeContext('linux', '/home/bob');
205
+ void dst.fs.writeFile('storages/mcp_connector.json', enc(JSON.stringify({
206
+ unit: { name: 'mcp_connector', version: 1 },
207
+ global: null,
208
+ tables: { connections: { 'keep-me': { key: 'keep-me', connectorId: 'other', serverName: 'other' } }, grants: { g1: { key: 'g1' } }, catalog: { c: 1 } },
209
+ })));
210
+ const items = await adapter.analyzeImport(exported.data, makeImportContext(dst, sections));
211
+ const create = items.find((i) => i.id === 'mcp-connector:github-github');
212
+ assert.equal(create?.kind, 'Warning', '备份里是占位符(token 被脱敏)→ 不写,要求重填');
213
+ assert.equal(create?.target, undefined);
214
+
215
+ // 用户重填后(模拟:把占位符换成真值)再导入 → Create 写回
216
+ const refilled = JSON.parse(JSON.stringify(exported.data)) as typeof exported.data;
217
+ const conn = refilled.connectors?.connections[0];
218
+ if (conn !== undefined) {
219
+ conn.bearerToken = 'ghp_' + 'Z'.repeat(36);
220
+ conn.headers = { Accept: 'application/json', Authorization: 'Bearer ghp_' + 'Z'.repeat(36) };
221
+ delete conn.redactedFields;
222
+ }
223
+ const items2 = await adapter.analyzeImport(refilled, makeImportContext(dst, new Map([['mcp', refilled]])));
224
+ const create2 = items2.find((i) => i.id === 'mcp-connector:github-github');
225
+ assert.equal(create2?.kind, 'Create');
226
+ assert.equal(create2?.target?.ref, 'storages/mcp_connector.json', '快照/回滚目标是整文件');
227
+ const r = await adapter.applyItem(create2!, makeImportContext(dst, new Map([['mcp', refilled]])));
228
+ assert.equal(r.ok, true);
229
+ assert.equal(r.needsRestart, true, '改 storage 文件必须重启 DSH 才生效');
230
+ const written = JSON.parse(new TextDecoder().decode(await dst.fs.readFile('storages/mcp_connector.json'))) as Record<string, any>;
231
+ assert.equal(written['tables']['connections']['github-github']['serverName'], 'github');
232
+ assert.deepEqual(written['tables']['grants'], { g1: { key: 'g1' } }, 'grants 等其它表必须原样保留');
233
+ assert.deepEqual(written['tables']['catalog'], { c: 1 }, 'catalog 等其它表必须原样保留');
234
+ assert.equal(written['tables']['connections']['keep-me']['connectorId'], 'other', '目标机既有连接一条都不能丢');
235
+
236
+ const items3 = await adapter.analyzeImport(refilled, makeImportContext(dst, new Map([['mcp', refilled]])));
237
+ assert.equal(items3.find((i) => i.id === 'mcp-connector:github-github')?.kind, 'Skip', '同内容再来一次 → Skip(幂等)');
238
+ });
239
+
240
+ test('mcp 连接器:目标存储解析不了 → 只报 warning,零写入', async () => {
241
+ const src = makeContext('win32', 'C:\\Users\\alice');
242
+ seedStore(src, { 'github-github': githubConn() });
243
+ const adapter = new McpAdapter();
244
+ const exported = await adapter.export(src, { includeSecrets: false });
245
+ const refilled = JSON.parse(JSON.stringify(exported.data)) as typeof exported.data;
246
+ const conn = refilled.connectors?.connections[0];
247
+ if (conn !== undefined) delete conn.redactedFields, conn.bearerToken = 'x' + 'y'.repeat(40), conn.headers = { Accept: 'application/json' };
248
+ const dst = makeContext('linux', '/home/bob');
249
+ void dst.fs.writeFile('storages/mcp_connector.json', enc('{ broken'));
250
+ const items = await adapter.analyzeImport(refilled, makeImportContext(dst, new Map([['mcp', refilled]])));
251
+ const storeWarning = items.find((i) => i.id === 'mcp-connector:store');
252
+ assert.equal(storeWarning?.kind, 'Warning');
253
+ assert.equal(items.some((i) => i.kind === 'Create'), false, '目标读不出来时绝不写(整文件改写会毁掉其它表)');
254
+ });
255
+
256
+ test('mcp 连接器:同名 server 同时来自 patch 行与连接器 → 两条都留 + warning(不隐式合并)', async () => {
257
+ const src = makeContext('win32', 'C:\\Users\\alice');
258
+ src.patchFile.lines.set('mcp-github', {
259
+ lineId: 'mcp-github',
260
+ raw: { id: 'mcp-github', name: 'dsh-mcp-client', config: { serverName: 'github', command: 'npx', args: ['-y', 'gh-mcp'] } },
261
+ });
262
+ seedStore(src, { 'github-github': githubConn() });
263
+ const adapter = new McpAdapter();
264
+ const exported = await adapter.export(src, { includeSecrets: false });
265
+ assert.equal(exported.data.servers.length, 1);
266
+ assert.equal(exported.data.connectors?.connections.length, 1);
267
+ const items = await adapter.analyzeImport(exported.data, makeImportContext(makeContext('linux', '/home/bob'), new Map([['mcp', exported.data]])));
268
+ const dupe = items.find((i) => i.id === 'mcp-connector-name:github-github');
269
+ assert.equal(dupe?.kind, 'Warning');
270
+ assert.match(String(dupe?.description), /github/);
271
+ });
272
+
273
+ test('mcp 连接器:OAuth 连接只提示重新授权,不进 connections', async () => {
274
+ const src = makeContext('win32', 'C:\\Users\\alice');
275
+ seedStore(src, {
276
+ 'github-github': githubConn(),
277
+ 'linear-linear': { key: 'linear-linear', connectorId: 'linear', kind: 'oauth', name: 'Linear', serverKey: 'linear', serverName: 'linear', transport: 'streamable-http', url: 'https://mcp.linear.app/sse', enabled: true, auth: { mode: 'oauth', grantKey: 'g-lin' }, createdAt: 1, updatedAt: 2 },
278
+ });
279
+ const adapter = new McpAdapter();
280
+ const exported = await adapter.export(src, { includeSecrets: false });
281
+ assert.equal(exported.data.connectors?.connections.length, 1, 'oauth 不进 connections(token 不在备份里,导进去也起不来)');
282
+ assert.equal(exported.data.connectors?.oauthConnections.length, 1);
283
+ assert.equal(JSON.stringify(exported.data).includes('g-lin'), false, 'grantKey 永不导出');
284
+ const items = await adapter.analyzeImport(exported.data, makeImportContext(makeContext('linux', '/home/bob'), new Map([['mcp', exported.data]])));
285
+ const oauth = items.find((i) => i.id === 'mcp-connector:linear-linear');
286
+ assert.equal(oauth?.kind, 'Warning');
287
+ assert.match(String(oauth?.description), /重新授权/);
288
+ });
289
+
290
+ test('mcp: validate 接受可选 connectors,形态不对才报错', async () => {
291
+ const adapter = new McpAdapter();
292
+ const ok = await adapter.validate({ version: 1, servers: [], connectors: { format: 'x', version: 1, connections: [{ key: 'k', connectorId: 'c', kind: 'manual', name: 'n', serverName: 's', transport: 'stdio', enabled: true }] } as never });
293
+ assert.equal(ok.valid, true);
294
+ const bad = await adapter.validate({ version: 1, servers: [], connectors: { connections: [{ key: '', serverName: '' }] } as never });
295
+ assert.equal(bad.valid, false);
296
+ });
297
+
298
+ /* ---------------------------------------------------- 条目级选择(展开 / 收起 / 逐项勾选) */
299
+
300
+ /** 同时有 patch 行 server 与连接器(含 OAuth 引用)的来源上下文 */
301
+ function seedBothSources(): ReturnType<typeof makeContext> {
302
+ const src = makeContext('win32', 'C:\\Users\\alice');
303
+ src.patchFile.lines.set('mcp-fs', {
304
+ lineId: 'mcp-fs',
305
+ raw: { id: 'mcp-fs', name: 'dsh-mcp-client', config: { serverName: 'filesystem', command: 'npx', args: ['-y', 'server-fs'] } },
306
+ });
307
+ seedStore(src, {
308
+ 'github-github': githubConn(),
309
+ 'linear-linear': { key: 'linear-linear', connectorId: 'linear', kind: 'oauth', name: 'Linear', serverKey: 'linear', serverName: 'linear', transport: 'streamable-http', url: 'https://mcp.linear.app/sse', enabled: true, auth: { mode: 'oauth', grantKey: 'g-lin' }, createdAt: 1, updatedAt: 2 },
310
+ });
311
+ return src;
312
+ }
313
+
314
+ test('mcp 条目级:listUnits 同时列出 patch 行、连接器与 OAuth 引用,id 前缀分明', async () => {
315
+ const src = seedBothSources();
316
+ const adapter = new McpAdapter();
317
+ const exported = await adapter.export(src, { includeSecrets: false });
318
+ const units = adapter.listUnits(exported);
319
+ assert.deepEqual(units.map((u) => u.id).sort(), [
320
+ 'mcp:connector:github-github',
321
+ 'mcp:connector:linear-linear',
322
+ 'mcp:patch:filesystem',
323
+ ]);
324
+ const fsUnit = units.find((u) => u.id === 'mcp:patch:filesystem');
325
+ assert.equal(fsUnit?.label, 'filesystem');
326
+ assert.equal(fsUnit?.detail, 'npx -y server-fs', 'stdio 单元给出命令 + 参数');
327
+ const gh = units.find((u) => u.id === 'mcp:connector:github-github');
328
+ assert.equal(gh?.label, 'GitHub');
329
+ assert.match(String(gh?.detail), /streamable-http https:\/\/api\.githubcopilot\.com/);
330
+ const lin = units.find((u) => u.id === 'mcp:connector:linear-linear');
331
+ assert.equal(lin?.detail, 'oauth linear', 'OAuth 连接只带引用,detail 标明它要靠重新授权');
332
+ assert.ok(units.every((u) => u.sizeBytes > 0), '每个单元都要有体积,选择器才能显示大小');
333
+ assert.equal(JSON.stringify(units).includes(GH_TOKEN), false, '单元清单不得带出凭据');
334
+ });
335
+
336
+ test('mcp 条目级:export 只带被勾选的单元(patch 行与连接器互不影响)', async () => {
337
+ const src = seedBothSources();
338
+ const adapter = new McpAdapter();
339
+ const onlyConnector = await adapter.export(src, { includeSecrets: false, includeItems: { mcp: ['mcp:connector:github-github'] } });
340
+ assert.equal(onlyConnector.data.servers.length, 0, '没勾 patch 行就不带 patch 行');
341
+ assert.deepEqual(onlyConnector.data.connectors?.connections.map((c) => c.key), ['github-github']);
342
+ assert.equal(onlyConnector.data.connectors?.oauthConnections.length, 0);
343
+ assert.equal(onlyConnector.counts['connectors'], 1);
344
+ assert.equal(onlyConnector.counts['servers'], 0);
345
+
346
+ const onlyPatch = await adapter.export(src, { includeSecrets: false, includeItems: { mcp: ['mcp:patch:filesystem'] } });
347
+ assert.deepEqual(onlyPatch.data.servers.map((s) => s.serverName), ['filesystem']);
348
+ assert.equal(onlyPatch.data.connectors, undefined, '一条连接器都没勾 → connectors 字段整个消失(不留空块冒充已备份)');
349
+ assert.equal(onlyPatch.counts['connectors'], undefined);
350
+ });
351
+
352
+ test('mcp 条目级:空白名单 = 一条都不带(不静默产出空分区)', async () => {
353
+ const src = seedBothSources();
354
+ const adapter = new McpAdapter();
355
+ const none = await adapter.export(src, { includeSecrets: false, includeItems: { mcp: [] } });
356
+ assert.deepEqual(none.data.servers, []);
357
+ assert.equal(none.data.connectors, undefined);
358
+ assert.equal(none.counts['servers'], 0);
359
+ });
360
+
361
+ test('mcp 条目级:单元 id 与 export 自洽(勾哪个就带出哪个)', async () => {
362
+ const src = seedBothSources();
363
+ const adapter = new McpAdapter();
364
+ const exported = await adapter.export(src, { includeSecrets: false });
365
+ for (const unit of adapter.listUnits(exported)) {
366
+ const picked = await adapter.export(src, { includeSecrets: false, includeItems: { mcp: [unit.id] } });
367
+ const inServers = picked.data.servers.some((s) => mcpServerUnitId(s.serverName) === unit.id);
368
+ const inConnections = (picked.data.connectors?.connections ?? []).some((c) => mcpConnectorUnitId(c.key) === unit.id);
369
+ const inOauth = (picked.data.connectors?.oauthConnections ?? []).some((c) => mcpConnectorUnitId(c.key) === unit.id);
370
+ assert.equal(inServers || inConnections || inOauth, true, '单元 ' + unit.id + ' 被列出,勾选后却不在导出里');
371
+ }
372
+ });
373
+