dsh-config-manager 0.1.77 → 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 (295) 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.d.ts +16 -0
  10. package/lib/adapters/plugins.js +234 -22
  11. package/lib/adapters/pnpm-workspace.d.ts +32 -0
  12. package/lib/adapters/pnpm-workspace.js +113 -2
  13. package/lib/adapters/prompts.d.ts +6 -2
  14. package/lib/adapters/prompts.js +17 -2
  15. package/lib/adapters/providers.d.ts +19 -2
  16. package/lib/adapters/providers.js +59 -3
  17. package/lib/adapters/settings.d.ts +6 -2
  18. package/lib/adapters/settings.js +13 -2
  19. package/lib/adapters/skills.d.ts +25 -1
  20. package/lib/adapters/skills.js +60 -0
  21. package/lib/adapters/ui.d.ts +6 -2
  22. package/lib/adapters/ui.js +13 -2
  23. package/lib/adapters/units.d.ts +18 -0
  24. package/lib/adapters/units.js +39 -0
  25. package/lib/adapters/workspaces.js +2 -1
  26. package/lib/client.d.ts +400 -112
  27. package/lib/client.js +2849 -1528
  28. package/lib/core/analyzer.js +3 -1
  29. package/lib/core/backup.d.ts +9 -0
  30. package/lib/core/backup.js +36 -1
  31. package/lib/core/mcp-connector-store.d.ts +159 -0
  32. package/lib/core/mcp-connector-store.js +445 -0
  33. package/lib/core/messages-core.d.ts +2 -0
  34. package/lib/core/messages-core.js +4 -0
  35. package/lib/core/messages.d.ts +30 -0
  36. package/lib/core/messages.js +64 -0
  37. package/lib/core/phase3-host.js +18 -1
  38. package/lib/core/plugin-cli.d.ts +48 -2
  39. package/lib/core/plugin-cli.js +84 -5
  40. package/lib/core/recovery-orchestrator.d.ts +3 -1
  41. package/lib/core/recovery-orchestrator.js +75 -16
  42. package/lib/core/rollback-activity.d.ts +89 -0
  43. package/lib/core/rollback-activity.js +350 -0
  44. package/lib/core/rollback-ledger.d.ts +0 -9
  45. package/lib/core/rollback-ledger.js +37 -0
  46. package/lib/core/rollback-merge.d.ts +22 -2
  47. package/lib/core/rollback-merge.js +63 -13
  48. package/lib/core/rollback-point.d.ts +93 -0
  49. package/lib/core/rollback-point.js +420 -101
  50. package/lib/core/rollback-undo-journal.d.ts +123 -0
  51. package/lib/core/rollback-undo-journal.js +517 -0
  52. package/lib/core/self-update.js +2 -4
  53. package/lib/core/text-fidelity.d.ts +89 -0
  54. package/lib/core/text-fidelity.js +176 -0
  55. package/lib/core/types.d.ts +22 -3
  56. package/lib/core/update-check.js +2 -4
  57. package/lib/core/validator.d.ts +9 -0
  58. package/lib/core/validator.js +12 -0
  59. package/lib/foreign/bundle.js +42 -3
  60. package/lib/foreign/claude-code.js +358 -26
  61. package/lib/foreign/claude-sessions.d.ts +51 -8
  62. package/lib/foreign/claude-sessions.js +111 -13
  63. package/lib/foreign/read-claude-code.d.ts +33 -4
  64. package/lib/foreign/read-claude-code.js +446 -65
  65. package/lib/foreign/read-claude-plugins.d.ts +36 -0
  66. package/lib/foreign/read-claude-plugins.js +403 -0
  67. package/lib/foreign/registry.js +25 -2
  68. package/lib/foreign/truth-table.js +6 -4
  69. package/lib/foreign/types.d.ts +170 -2
  70. package/lib/index.d.ts +14 -5
  71. package/lib/index.js +60 -16
  72. package/lib/market/git-file-writer.js +7 -1
  73. package/lib/profiles/dsh-profile-launcher.d.ts +20 -0
  74. package/lib/profiles/dsh-profile-launcher.js +58 -1
  75. package/lib/profiles/dsh-profile-shared.d.ts +8 -2
  76. package/lib/profiles/dsh-profile-shared.js +8 -2
  77. package/lib/routes/foreign.js +8 -1
  78. package/lib/routes/index.d.ts +1 -1
  79. package/lib/routes/kit.d.ts +2 -2
  80. package/lib/routes/sync.js +173 -9
  81. package/lib/schema/manifest.d.ts +6 -1
  82. package/lib/schema/manifest.js +55 -0
  83. package/lib/schema/types.d.ts +61 -2
  84. package/lib/sync/autosync-config.d.ts +73 -0
  85. package/lib/sync/autosync-config.js +89 -0
  86. package/lib/sync/autosync-scheduler.d.ts +80 -6
  87. package/lib/sync/autosync-scheduler.js +263 -40
  88. package/lib/sync/gist/gist-transport.d.ts +15 -1
  89. package/lib/sync/gist/gist-transport.js +66 -5
  90. package/lib/sync/gist/messages.d.ts +3 -0
  91. package/lib/sync/gist/messages.js +7 -0
  92. package/lib/sync/git/git-transport.d.ts +42 -3
  93. package/lib/sync/git/git-transport.js +146 -3
  94. package/lib/sync/merge.d.ts +47 -14
  95. package/lib/sync/merge.js +242 -222
  96. package/lib/sync/messages.d.ts +8 -0
  97. package/lib/sync/messages.js +19 -0
  98. package/lib/sync/risk.d.ts +19 -2
  99. package/lib/sync/risk.js +10 -3
  100. package/lib/sync/s3/messages.d.ts +6 -0
  101. package/lib/sync/s3/messages.js +13 -0
  102. package/lib/sync/s3/s3-transport.d.ts +9 -1
  103. package/lib/sync/s3/s3-transport.js +78 -6
  104. package/lib/sync/sync-engine.d.ts +193 -4
  105. package/lib/sync/sync-engine.js +450 -37
  106. package/lib/sync/sync-history.d.ts +11 -0
  107. package/lib/sync/sync-history.js +20 -0
  108. package/lib/sync/sync-state.d.ts +43 -0
  109. package/lib/sync/sync-state.js +44 -0
  110. package/lib/sync/sync-watcher.d.ts +76 -0
  111. package/lib/sync/sync-watcher.js +202 -0
  112. package/lib/sync/text-merge.d.ts +85 -0
  113. package/lib/sync/text-merge.js +513 -0
  114. package/lib/sync/transport.d.ts +48 -0
  115. package/lib/sync/transport.js +12 -0
  116. package/lib/sync/watch-scope.d.ts +38 -0
  117. package/lib/sync/watch-scope.js +95 -0
  118. package/lib/sync/webdav/webdav-transport.d.ts +16 -2
  119. package/lib/sync/webdav/webdav-transport.js +128 -11
  120. package/lib/ui/foreign-view.js +10 -0
  121. package/lib/ui/i18n.d.ts +8 -0
  122. package/lib/ui/i18n.js +20 -0
  123. package/lib/ui/rollback-view.d.ts +12 -0
  124. package/lib/ui/rollback-view.js +2 -0
  125. package/lib/ui/sync-settings-view.d.ts +47 -0
  126. package/lib/ui/sync-settings-view.js +24 -0
  127. package/lib/ui/types.d.ts +21 -0
  128. package/lib/utils/env-lock.d.ts +1 -0
  129. package/lib/utils/env-lock.js +7 -4
  130. package/package.json +6 -6
  131. package/src/adapters/credentials.ts +22 -4
  132. package/src/adapters/file-collection.ts +2 -1
  133. package/src/adapters/mcp.test.ts +235 -1
  134. package/src/adapters/mcp.ts +276 -5
  135. package/src/adapters/messages.ts +7 -0
  136. package/src/adapters/plugin-files.ts +2 -1
  137. package/src/adapters/plugins.test.ts +338 -2
  138. package/src/adapters/plugins.ts +243 -23
  139. package/src/adapters/pnpm-workspace.test.ts +89 -1
  140. package/src/adapters/pnpm-workspace.ts +127 -2
  141. package/src/adapters/prompts.ts +20 -3
  142. package/src/adapters/providers.ts +62 -4
  143. package/src/adapters/settings.ts +16 -3
  144. package/src/adapters/skills.test.ts +141 -0
  145. package/src/adapters/skills.ts +58 -2
  146. package/src/adapters/ui.ts +16 -3
  147. package/src/adapters/units-entries.test.ts +190 -0
  148. package/src/adapters/units.ts +50 -0
  149. package/src/adapters/workspaces.ts +2 -1
  150. package/src/client/about/PluginUpdateDialog.tsx +4 -2
  151. package/src/client/about/ReleaseNotesDialog.tsx +4 -2
  152. package/src/client/chat/RollbackReviewDialog.tsx +3 -2
  153. package/src/client/common/Skeleton.tsx +32 -0
  154. package/src/client/common/routes.ts +9 -0
  155. package/src/client/config-manager.module.css +321 -11
  156. package/src/client/home/HomePanel.tsx +14 -6
  157. package/src/client/import/ForeignImportView.tsx +3 -1
  158. package/src/client/import/import-wizard-steps.tsx +2 -1
  159. package/src/client/library/ArtifactRow.tsx +12 -2
  160. package/src/client/library/LibraryActions.tsx +6 -3
  161. package/src/client/library/LibraryPanel.tsx +21 -3
  162. package/src/client/library/RestorePlanView.tsx +4 -2
  163. package/src/client/locales.ts +3 -0
  164. package/src/client/recovery/RecoveryPanel.tsx +54 -7
  165. package/src/client/recovery/RollbackCard.tsx +7 -2
  166. package/src/client/recovery/RollbackDiffPane.tsx +3 -2
  167. package/src/client/recovery/recovery-locales.ts +20 -2
  168. package/src/client/recovery/recovery-view.test.ts +39 -1
  169. package/src/client/recovery/recovery-view.ts +23 -1
  170. package/src/client/run-store.test.ts +1 -1
  171. package/src/client/run-store.ts +4 -0
  172. package/src/client/sync/AutosyncCard.tsx +49 -6
  173. package/src/client/sync/MergeModal.tsx +219 -0
  174. package/src/client/sync/MergePreviewView.tsx +306 -0
  175. package/src/client/sync/SyncActivityBar.tsx +31 -0
  176. package/src/client/sync/SyncConfirmView.tsx +2 -1
  177. package/src/client/sync/SyncPanel.tsx +130 -87
  178. package/src/client/sync/SyncProbeRow.tsx +162 -0
  179. package/src/client/sync/SyncSettingsView.test.ts +27 -2
  180. package/src/client/sync/WorkDirCard.tsx +44 -6
  181. package/src/client/sync/history-model.ts +4 -1
  182. package/src/client/sync/merge-view.test.ts +234 -0
  183. package/src/client/sync/merge-view.ts +371 -0
  184. package/src/client/sync/probe-view.test.ts +90 -0
  185. package/src/client/sync/probe-view.ts +63 -0
  186. package/src/client/sync/remote-device-view.test.ts +35 -0
  187. package/src/client/sync/remote-device-view.ts +25 -0
  188. package/src/client/sync/sync-api.ts +212 -2
  189. package/src/client/sync/sync-entry.test.ts +78 -0
  190. package/src/client/sync/sync-entry.ts +53 -0
  191. package/src/client/sync/sync-locales.ts +234 -2
  192. package/src/client/sync/sync-view.test.ts +18 -1
  193. package/src/client/sync/sync-view.ts +20 -0
  194. package/src/core/analyzer.ts +3 -1
  195. package/src/core/backup.test.ts +41 -0
  196. package/src/core/backup.ts +36 -1
  197. package/src/core/mcp-connector-store.test.ts +265 -0
  198. package/src/core/mcp-connector-store.ts +516 -0
  199. package/src/core/messages-core.ts +4 -0
  200. package/src/core/messages.ts +66 -0
  201. package/src/core/phase3-host.ts +18 -1
  202. package/src/core/phase3-production-integration.test.ts +4 -0
  203. package/src/core/plugin-cli.fs.test.ts +65 -3
  204. package/src/core/plugin-cli.test.ts +26 -0
  205. package/src/core/plugin-cli.ts +85 -6
  206. package/src/core/recovery-orchestrator.test.ts +104 -5
  207. package/src/core/recovery-orchestrator.ts +74 -17
  208. package/src/core/rollback-activity.test.ts +311 -0
  209. package/src/core/rollback-activity.ts +367 -0
  210. package/src/core/rollback-cross-session.test.ts +694 -0
  211. package/src/core/rollback-interrupted.test.ts +137 -0
  212. package/src/core/rollback-ledger.ts +32 -0
  213. package/src/core/rollback-merge.test.ts +130 -0
  214. package/src/core/rollback-merge.ts +78 -14
  215. package/src/core/rollback-point.ts +468 -91
  216. package/src/core/rollback-undo-journal.test.ts +508 -0
  217. package/src/core/rollback-undo-journal.ts +571 -0
  218. package/src/core/self-update.ts +2 -5
  219. package/src/core/text-fidelity.test.ts +189 -0
  220. package/src/core/text-fidelity.ts +197 -0
  221. package/src/core/types.ts +21 -3
  222. package/src/core/update-check.ts +2 -5
  223. package/src/core/validator.ts +14 -0
  224. package/src/foreign/bundle-origin.test.ts +265 -0
  225. package/src/foreign/bundle.ts +45 -5
  226. package/src/foreign/claude-code.test.ts +268 -0
  227. package/src/foreign/claude-code.ts +406 -25
  228. package/src/foreign/claude-plugins.test.ts +234 -0
  229. package/src/foreign/claude-sessions.test.ts +141 -0
  230. package/src/foreign/claude-sessions.ts +179 -23
  231. package/src/foreign/file-budget.test.ts +4 -1
  232. package/src/foreign/read-claude-code.test.ts +320 -0
  233. package/src/foreign/read-claude-code.ts +490 -71
  234. package/src/foreign/read-claude-plugins.ts +465 -0
  235. package/src/foreign/registry.ts +25 -2
  236. package/src/foreign/session-ir.test.ts +65 -0
  237. package/src/foreign/truth-table.test.ts +81 -0
  238. package/src/foreign/truth-table.ts +6 -3
  239. package/src/foreign/types.ts +162 -2
  240. package/src/index.facade.test.ts +26 -4
  241. package/src/index.ts +76 -19
  242. package/src/market/git-file-writer.test.ts +4 -0
  243. package/src/market/git-file-writer.ts +7 -1
  244. package/src/profiles/dsh-profile-launcher.test.ts +68 -3
  245. package/src/profiles/dsh-profile-launcher.ts +56 -1
  246. package/src/profiles/dsh-profile-shared.ts +8 -2
  247. package/src/routes/foreign.ts +8 -1
  248. package/src/routes/index.ts +1 -1
  249. package/src/routes/kit.ts +2 -2
  250. package/src/routes/sync.ts +176 -9
  251. package/src/schema/manifest-schema.test.ts +87 -2
  252. package/src/schema/manifest.ts +57 -1
  253. package/src/schema/types.ts +59 -2
  254. package/src/sync/autosync-config.test.ts +73 -1
  255. package/src/sync/autosync-config.ts +124 -0
  256. package/src/sync/autosync-risk-tier.test.ts +38 -2
  257. package/src/sync/autosync-scheduler.test.ts +264 -12
  258. package/src/sync/autosync-scheduler.ts +296 -44
  259. package/src/sync/gist/gist-conditional-write.test.ts +223 -0
  260. package/src/sync/gist/gist-transport.ts +73 -7
  261. package/src/sync/gist/messages.ts +7 -0
  262. package/src/sync/git/git-probe.test.ts +80 -0
  263. package/src/sync/git/git-transport.test.ts +185 -0
  264. package/src/sync/git/git-transport.ts +178 -5
  265. package/src/sync/merge.test.ts +188 -55
  266. package/src/sync/merge.ts +282 -224
  267. package/src/sync/messages.ts +19 -0
  268. package/src/sync/risk.ts +29 -5
  269. package/src/sync/s3/messages.ts +13 -0
  270. package/src/sync/s3/s3-conditional-write.test.ts +330 -0
  271. package/src/sync/s3/s3-transport.ts +87 -9
  272. package/src/sync/sync-device-identity.test.ts +267 -0
  273. package/src/sync/sync-engine.test.ts +379 -0
  274. package/src/sync/sync-engine.ts +535 -36
  275. package/src/sync/sync-history.test.ts +16 -1
  276. package/src/sync/sync-history.ts +26 -0
  277. package/src/sync/sync-state.test.ts +64 -0
  278. package/src/sync/sync-state.ts +79 -0
  279. package/src/sync/sync-watcher.test.ts +197 -0
  280. package/src/sync/sync-watcher.ts +246 -0
  281. package/src/sync/text-merge.test.ts +358 -0
  282. package/src/sync/text-merge.ts +554 -0
  283. package/src/sync/transport.ts +57 -0
  284. package/src/sync/watch-scope.test.ts +122 -0
  285. package/src/sync/watch-scope.ts +120 -0
  286. package/src/sync/webdav/webdav-conditional-write.test.ts +360 -0
  287. package/src/sync/webdav/webdav-transport.ts +137 -14
  288. package/src/ui/foreign-view.test.ts +34 -0
  289. package/src/ui/foreign-view.ts +10 -0
  290. package/src/ui/i18n.ts +20 -0
  291. package/src/ui/rollback-view.ts +14 -0
  292. package/src/ui/sync-settings-view.test.ts +28 -1
  293. package/src/ui/sync-settings-view.ts +43 -0
  294. package/src/ui/types.ts +22 -0
  295. package/src/utils/env-lock.ts +8 -5
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import type { CredentialsSection } from '../schema/types.ts';
11
11
  import type { MsgFunc } from '../core/messages.ts';
12
- import type { ApplyResult, ConfigAdapter, ExportOptions, ExportSection, HostContext, ImportContext, PlanItem, ValidationResult } from '../core/types.ts';
12
+ import type { ApplyResult, ConfigAdapter, ExportOptions, ExportSection, ExportUnit, HostContext, ImportContext, PlanItem, ValidationResult } from '../core/types.ts';
13
13
  import { type NamespaceProvider } from './settings.ts';
14
14
  export type CredentialRefsProvider = (ctx: HostContext) => Promise<string[]>;
15
15
  /** 缺省 ref 收集:遍历 settings namespace,收集 llm apiKeyEnv / providers[].apiKeyEnv,
@@ -30,7 +30,11 @@ export declare class CredentialsAdapter implements ConfigAdapter<CredentialsSect
30
30
  readonly portability: import("../schema/section-registry.ts").SectionPortability;
31
31
  private readonly refs;
32
32
  constructor(options?: CredentialsAdapterOptions);
33
- export(ctx: HostContext, _options: ExportOptions): Promise<ExportSection<CredentialsSection>>;
33
+ export(ctx: HostContext, options: ExportOptions): Promise<ExportSection<CredentialsSection>>;
34
+ /** 单元 id:`credentialsStatus:<凭据 ref>` */
35
+ private unitId;
36
+ /** 条目级单元清单(零 I/O);label = 凭据 ref,detail = 本机是否有值(不含任何敏感值) */
37
+ listUnits(section: ExportSection<CredentialsSection>): ExportUnit[];
34
38
  analyzeImport(_data: CredentialsSection, _ctx: ImportContext): Promise<PlanItem[]>;
35
39
  applyItem(item: PlanItem, ctx: ImportContext): Promise<ApplyResult>;
36
40
  validate(data: CredentialsSection, msg?: MsgFunc): Promise<ValidationResult>;
@@ -2,6 +2,7 @@ import { msgOf, zhMsg } from '../core/messages.js';
2
2
  import { namespaceFromMap, readNamespaceMap, resolveNamespaces } from './settings.js';
3
3
  import { sectionMeta } from '../schema/section-registry.js';
4
4
  import { validateJsonSection } from './json-section.js';
5
+ import { entryByteSize, filterByUnits } from './units.js';
5
6
  /** 缺省 ref 收集:遍历 settings namespace,收集 llm apiKeyEnv / providers[].apiKeyEnv,
6
7
  * 以及 secrets 标记中「引用类字段」(apiKeyEnv/tokenEnv…)的字段值。
7
8
  * 注意 secrets[].path[0] 是 settings 文档内的字段路径(如 ['apiKey']),本身不是凭据 ref(设计 §4.2),
@@ -60,7 +61,7 @@ export class CredentialsAdapter {
60
61
  constructor(options = {}) {
61
62
  this.refs = options.refs ?? defaultCredentialRefs(options.namespaces ?? []);
62
63
  }
63
- async export(ctx, _options) {
64
+ async export(ctx, options) {
64
65
  const credentials = [];
65
66
  const warnings = [];
66
67
  for (const ref of await this.refs(ctx)) {
@@ -78,13 +79,28 @@ export class CredentialsAdapter {
78
79
  warnings.push(msgOf(ctx)('adapter.credStatusReadFailed', { ref, reason: err instanceof Error ? err.message : String(err) }));
79
80
  }
80
81
  }
82
+ // 条目级白名单:未勾选的凭据 ref 不进「需补录」清单(少带一条 = 少录一个值)
83
+ const picked = filterByUnits(credentials, options.includeItems?.[this.id], (c) => this.unitId(c.ref));
81
84
  return {
82
85
  sectionId: 'credentialsStatus',
83
- data: { version: 1, credentials },
84
- counts: { credentials: credentials.length },
86
+ data: { version: 1, credentials: picked },
87
+ counts: { credentials: picked.length },
85
88
  warnings,
86
89
  };
87
90
  }
91
+ /** 单元 id:`credentialsStatus:<凭据 ref>` */
92
+ unitId(ref) {
93
+ return this.id + ':' + ref;
94
+ }
95
+ /** 条目级单元清单(零 I/O);label = 凭据 ref,detail = 本机是否有值(不含任何敏感值) */
96
+ listUnits(section) {
97
+ return (section.data.credentials ?? []).map((c) => ({
98
+ id: this.unitId(c.ref),
99
+ label: c.ref,
100
+ detail: c.configured ? 'configured' : 'missing',
101
+ sizeBytes: entryByteSize(c),
102
+ }));
103
+ }
88
104
  async analyzeImport(_data, _ctx) {
89
105
  // MissingSecret 计划项由引擎在 createImportPlan 兜底生成(analyzer.ensureMissingSecrets),
90
106
  // 依据 = 本分区里 configured=true 的凭据 → 用户补录清单。这里保持零写入纯计算。
@@ -287,7 +287,8 @@ export class FileCollectionAdapter {
287
287
  }
288
288
  /** 单元清单(零 I/O:直接由 export 产物归并)。 */
289
289
  listUnits(section) {
290
- return unitsFromFiles(this.id, section.data.files, (rel) => this.unitIdOf(rel));
290
+ // files 可能缺省(空分区 / 手写 section):按空表处理,不抛错 —— 选择器对任何分区都会调 listUnits
291
+ return unitsFromFiles(this.id, section.data.files ?? [], (rel) => this.unitIdOf(rel));
291
292
  }
292
293
  /**
293
294
  * 幂等比对用的内容(相对 baseDir 路径 + 盘上/包内字节 → 参与哈希比对的字节)。
@@ -1,6 +1,7 @@
1
1
  import type { MsgFunc } from '../core/messages.ts';
2
2
  import type { McpServerEntry } from '../schema/types.ts';
3
- import type { ApplyResult, ConfigAdapter, ExportOptions, ExportSection, HostContext, ImportContext, PlanItem, ValidationResult } from '../core/types.ts';
3
+ import type { ApplyResult, ConfigAdapter, ExportOptions, ExportSection, ExportUnit, HostContext, ImportContext, PlanItem, ValidationResult } from '../core/types.ts';
4
+ import type { McpConnectorExport } from '../core/mcp-connector-store.ts';
4
5
  /** 导出记录:McpServerEntry 之外附加来源 patch 行 id 与所在层(导入写回定位用) */
5
6
  export interface McpExportEntry extends McpServerEntry {
6
7
  sourceLineId: string;
@@ -13,6 +14,13 @@ export interface McpExportEntry extends McpServerEntry {
13
14
  export interface McpExportSection {
14
15
  version: 1;
15
16
  servers: McpExportEntry[];
17
+ /**
18
+ * MCP 连接器(\`dsh-mcp-connector\` 插件)的连接,第二个数据源。
19
+ *
20
+ * 为什么并列而不是新增分区:两条路径在 DSH 里都是「MCP 服务器」,用户心智里是一件事;
21
+ * 而备份格式是追加字段 ⇒ 旧备份没有它 = 空,读旧包的代码不受影响。
22
+ */
23
+ connectors?: McpConnectorExport;
16
24
  }
17
25
  /**
18
26
  * 从 patch 行数组中提取 MCP server 条目(以 config.serverName 存在为判定)。
@@ -26,13 +34,51 @@ export declare function extractMcpServers(lines: {
26
34
  }[]): McpExportEntry[];
27
35
  /** 由导出条目构造写回 patch 行 raw(单行形态,与 dsh-mcp-client 组合 config 语义一致) */
28
36
  export declare function buildMcpPatchLine(lineId: string, server: McpServerEntry): Record<string, unknown>;
37
+ /**
38
+ * 单元 id:两条数据源各自独立前缀。
39
+ *
40
+ * 为什么必须分开:patch 行的 `serverName` 与连接器的 `serverName` 完全可能同名(同一台服务器的两条
41
+ * 历史配置路径),共用 id 会让「勾了其中一个」变成「两个一起勾」,与选择器「逐项可勾」的前提冲突。
42
+ * 命名空间仍以 `mcp:` 开头 —— 与 ExportUnit 的「<section>:<单元>」约定一致。
43
+ */
44
+ export declare function mcpServerUnitId(serverName: string): string;
45
+ export declare function mcpConnectorUnitId(key: string): string;
29
46
  export declare class McpAdapter implements ConfigAdapter<McpExportSection> {
30
47
  readonly id: 'mcp';
31
48
  readonly displayName: string;
32
49
  readonly defaultIncluded: boolean;
33
50
  readonly portability: import("../schema/section-registry.ts").SectionPortability;
34
- export(ctx: HostContext, _options: ExportOptions): Promise<ExportSection<McpExportSection>>;
51
+ export(ctx: HostContext, options: ExportOptions): Promise<ExportSection<McpExportSection>>;
52
+ /**
53
+ * 条目级单元清单(选择器靠它决定「能不能展开」)。
54
+ *
55
+ * 契约(core/types.ts:1211):纯函数、零 I/O,且必须与 export(includeItems) 自洽 ——
56
+ * 单元 id 就是 export 白名单里认的 id,两边共用 mcpServerUnitId / mcpConnectorUnitId。
57
+ *
58
+ * 单元 label 与 detail 只放「能帮用户认出是哪条」的信息:连接器的 detail 复用
59
+ * describeConnector(stdio 给命令 + 参数、http 给 URL),**绝不**带出 env / header 值。
60
+ */
61
+ listUnits(section: ExportSection<McpExportSection>): ExportUnit[];
35
62
  analyzeImport(data: McpExportSection, ctx: ImportContext): Promise<PlanItem[]>;
63
+ /**
64
+ * 连接器来源的计划项。
65
+ *
66
+ * 三条纪律:
67
+ * ① 目标存储「存在但解析不了」→ 只报 warning、**零写入**(整文件改写会把 grants 等表一起抹掉);
68
+ * ② 条目里还有占位符 → 不写,产出「需重填凭据」的 warning(写进去 = 给用户一个起不来的 Server);
69
+ * ③ 同名 server 同时来自 patch 行与连接器 → 两条都保留,另加一条 warning(不隐式合并,
70
+ * 两者在 DSH 里是两份独立配置,合并会悄悄改变用户的生效结果)。
71
+ */
72
+ private analyzeConnectors;
36
73
  applyItem(item: PlanItem, ctx: ImportContext): Promise<ApplyResult>;
74
+ /**
75
+ * 写入一条连接:读全文件 → 只替换 `tables.connections[<key>]` → 整文件原子写。
76
+ *
77
+ * 为什么整文件写而不是走宿主 storage 服务:本机没装 `@deepseek-ai/dsh-storage-domain`,
78
+ * 我们拿不到 `mcp_connector` 域的表句柄;而直接改文件又会被运行中的宿主内存态覆盖 ⇒
79
+ * 计划项与本结果都显式要求重启(needsRestart),写之前先把原文件整体快照(引擎侧按
80
+ * `storages/` 前缀走文件类快照)。
81
+ */
82
+ private applyConnectorItem;
37
83
  validate(data: McpExportSection, msg?: MsgFunc): Promise<ValidationResult>;
38
84
  }
@@ -16,6 +16,7 @@ import { msgOf, zhMsg } from '../core/messages.js';
16
16
  import { sectionMeta } from '../schema/section-registry.js';
17
17
  import { validateJsonSection } from './json-section.js';
18
18
  import { locatePatchLineLayer, readEffectivePatchLines, resolveWriteLayer } from '../core/patch-layers.js';
19
+ import { MCP_CONNECTOR_STORE_REL, buildConnectorRecord, comparableConnector, exportConnectorBlock, findConnectorRecord, mergeConnectorIntoStoreText, pickConnectorKey, readConnectorStore, refillFieldsOf, } from '../core/mcp-connector-store.js';
19
20
  /**
20
21
  * 从 patch 行数组中提取 MCP server 条目(以 config.serverName 存在为判定)。
21
22
  *
@@ -83,17 +84,62 @@ export function buildMcpPatchLine(lineId, server) {
83
84
  }
84
85
  return { id: lineId, name: 'dsh-mcp-client', config };
85
86
  }
87
+ /** 连接器计划项的 id 前缀(导入时按前缀分派到 applyConnectorItem) */
88
+ const MCP_CONNECTOR_ITEM_PREFIX = 'mcp-connector:';
89
+ /** 计划项 detail:stdio 给命令 + 参数,http 给 URL(不打印任何 header/env 值,防凭据入日志) */
90
+ function describeConnector(entry) {
91
+ const head = entry.transport === 'stdio'
92
+ ? [entry.command ?? '', ...(entry.args ?? [])].filter((s) => s !== '').join(' ')
93
+ : (entry.url ?? '');
94
+ return `${entry.transport} ${head}`.trim().slice(0, 200);
95
+ }
86
96
  function newLineId(serverName) {
87
97
  const slug = serverName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
88
98
  return `mcp-${slug || 'server'}`;
89
99
  }
100
+ /**
101
+ * 单元 id:两条数据源各自独立前缀。
102
+ *
103
+ * 为什么必须分开:patch 行的 `serverName` 与连接器的 `serverName` 完全可能同名(同一台服务器的两条
104
+ * 历史配置路径),共用 id 会让「勾了其中一个」变成「两个一起勾」,与选择器「逐项可勾」的前提冲突。
105
+ * 命名空间仍以 `mcp:` 开头 —— 与 ExportUnit 的「<section>:<单元>」约定一致。
106
+ */
107
+ export function mcpServerUnitId(serverName) {
108
+ return 'mcp:patch:' + serverName;
109
+ }
110
+ export function mcpConnectorUnitId(key) {
111
+ return 'mcp:connector:' + key;
112
+ }
113
+ /** 单元体积 = 该条目在包里的 UTF-8 字节数(与 /export-preview 对 JSON 分区的口径一致) */
114
+ function unitBytes(value) {
115
+ return new TextEncoder().encode(JSON.stringify(value) ?? '').byteLength;
116
+ }
117
+ /** 只读的 stdio 描述(命令 + 参数),不打印任何 env / header 值 */
118
+ function stdioDetail(entry) {
119
+ return [entry.command ?? '', ...(entry.args ?? [])].filter((s) => s !== '').join(' ').slice(0, 200);
120
+ }
121
+ /**
122
+ * 连接器整块的条目级过滤。
123
+ *
124
+ * allow === undefined = 全量(旧调用方零改动);过滤后一条不剩 → 返回 undefined,
125
+ * 让 `connectors` 字段整个消失(而不是留一个空块冒充「连接器已备份」)。
126
+ */
127
+ function filterConnectorBlock(block, allow) {
128
+ if (allow === undefined)
129
+ return block;
130
+ const connections = block.connections.filter((c) => allow.includes(mcpConnectorUnitId(c.key)));
131
+ const oauthConnections = block.oauthConnections.filter((c) => allow.includes(mcpConnectorUnitId(c.key)));
132
+ if (connections.length === 0 && oauthConnections.length === 0)
133
+ return undefined;
134
+ return { ...block, connections, oauthConnections };
135
+ }
90
136
  export class McpAdapter {
91
137
  id = 'mcp';
92
138
  // 元数据唯一来源 = 注册表(t31):不再与 ui/export-flow.ts 的导出目录各写一份
93
139
  displayName = sectionMeta('mcp').displayName;
94
140
  defaultIncluded = sectionMeta('mcp').defaultIncluded;
95
141
  portability = sectionMeta('mcp').portability;
96
- async export(ctx, _options) {
142
+ async export(ctx, options) {
97
143
  const warnings = [];
98
144
  // issue #71:两层都读(home 层优先),否则 profile 层里的 server 全部丢失
99
145
  const read = await readEffectivePatchLines(ctx.patchFile, ctx.profile);
@@ -101,13 +147,94 @@ export class McpAdapter {
101
147
  warnings.push(msgOf(ctx)('adapter.patchReadFailedMCP', { reason: `${f.file}: ${f.reason}` }));
102
148
  }
103
149
  const servers = extractMcpServers(read.lines);
150
+ // 条目级白名单:undefined = 整分区全量(旧调用方零改动);空数组 = 一条都不带。
151
+ // 选择器把「部分勾选」翻译成 includeItems['mcp'],这里必须如实过滤,否则勾了等于没勾。
152
+ const allow = options.includeItems?.[this.id];
153
+ const pickedServers = allow === undefined
154
+ ? servers
155
+ : servers.filter((s) => allow.includes(mcpServerUnitId(s.serverName)));
156
+ // 第二个数据源:连接器插件的 `storages/mcp_connector.json`(用户实际配置 MCP 的地方)。
157
+ // 读不到 = 本机没装/没用连接器(正常,静默);读到了解析不了 = 异常,如实报 warning,
158
+ // **绝不**当作空表导出 —— 否则用户以为备份里有,恢复时才发现丢了。
159
+ let connectors;
160
+ try {
161
+ const store = await readConnectorStore(ctx.fs);
162
+ if (store.exists && !store.parsed) {
163
+ warnings.push(msgOf(ctx)('adapter.mcpConnectorReadFailed', { reason: store.error ?? 'unknown' }));
164
+ }
165
+ else if (store.exists && store.parsed) {
166
+ // 文件不存在 = 本机没有任何连接器:不写这个字段(旧备份也没有它,形状保持一致)
167
+ connectors = filterConnectorBlock(exportConnectorBlock(store.connections), allow);
168
+ }
169
+ }
170
+ catch (err) {
171
+ warnings.push(msgOf(ctx)('adapter.mcpConnectorReadFailed', { reason: err instanceof Error ? err.message : String(err) }));
172
+ }
173
+ const data = { version: 1, servers: pickedServers };
174
+ if (connectors !== undefined)
175
+ data.connectors = connectors;
104
176
  return {
105
177
  sectionId: 'mcp',
106
- data: { version: 1, servers },
107
- counts: { servers: servers.length },
178
+ data,
179
+ counts: {
180
+ servers: pickedServers.length,
181
+ ...(connectors !== undefined ? { connectors: connectors.connections.length } : {}),
182
+ },
108
183
  warnings,
109
184
  };
110
185
  }
186
+ /**
187
+ * 条目级单元清单(选择器靠它决定「能不能展开」)。
188
+ *
189
+ * 契约(core/types.ts:1211):纯函数、零 I/O,且必须与 export(includeItems) 自洽 ——
190
+ * 单元 id 就是 export 白名单里认的 id,两边共用 mcpServerUnitId / mcpConnectorUnitId。
191
+ *
192
+ * 单元 label 与 detail 只放「能帮用户认出是哪条」的信息:连接器的 detail 复用
193
+ * describeConnector(stdio 给命令 + 参数、http 给 URL),**绝不**带出 env / header 值。
194
+ */
195
+ listUnits(section) {
196
+ const units = [];
197
+ const seen = new Set();
198
+ for (const server of section.data.servers ?? []) {
199
+ const id = mcpServerUnitId(server.serverName);
200
+ if (seen.has(id))
201
+ continue;
202
+ seen.add(id);
203
+ units.push({
204
+ id,
205
+ label: server.serverName,
206
+ detail: server.type === 'stdio' ? stdioDetail(server) : (server.url ?? ''),
207
+ sizeBytes: unitBytes(server),
208
+ });
209
+ }
210
+ const connectors = section.data.connectors;
211
+ for (const connection of connectors?.connections ?? []) {
212
+ const id = mcpConnectorUnitId(connection.key);
213
+ if (seen.has(id))
214
+ continue;
215
+ seen.add(id);
216
+ units.push({
217
+ id,
218
+ label: connection.name,
219
+ detail: describeConnector(connection),
220
+ sizeBytes: unitBytes(connection),
221
+ });
222
+ }
223
+ for (const oauth of connectors?.oauthConnections ?? []) {
224
+ const id = mcpConnectorUnitId(oauth.key);
225
+ if (seen.has(id))
226
+ continue;
227
+ seen.add(id);
228
+ units.push({
229
+ id,
230
+ label: oauth.name,
231
+ // OAuth 连接只带引用(Token 永不进备份),detail 说明它靠重新授权才能用
232
+ detail: 'oauth ' + oauth.serverName,
233
+ sizeBytes: unitBytes(oauth),
234
+ });
235
+ }
236
+ return units;
237
+ }
111
238
  async analyzeImport(data, ctx) {
112
239
  const msg = ctx.msg;
113
240
  const items = [];
@@ -151,9 +278,95 @@ export class McpAdapter {
151
278
  }
152
279
  }
153
280
  }
281
+ items.push(...await this.analyzeConnectors(data, ctx));
282
+ return items;
283
+ }
284
+ /**
285
+ * 连接器来源的计划项。
286
+ *
287
+ * 三条纪律:
288
+ * ① 目标存储「存在但解析不了」→ 只报 warning、**零写入**(整文件改写会把 grants 等表一起抹掉);
289
+ * ② 条目里还有占位符 → 不写,产出「需重填凭据」的 warning(写进去 = 给用户一个起不来的 Server);
290
+ * ③ 同名 server 同时来自 patch 行与连接器 → 两条都保留,另加一条 warning(不隐式合并,
291
+ * 两者在 DSH 里是两份独立配置,合并会悄悄改变用户的生效结果)。
292
+ */
293
+ async analyzeConnectors(data, ctx) {
294
+ const msg = ctx.msg;
295
+ const block = data.connectors;
296
+ if (block === undefined)
297
+ return [];
298
+ const items = [];
299
+ const target = await readConnectorStore(ctx.target.fs);
300
+ if (target.exists && !target.parsed) {
301
+ items.push({
302
+ id: 'mcp-connector:store',
303
+ kind: 'Warning',
304
+ adapter: 'mcp',
305
+ description: msg('adapter.mcpConnectorTargetUnreadable', { reason: target.error ?? 'unknown' }),
306
+ severity: 'warning',
307
+ });
308
+ }
309
+ const writable = target.parsed;
310
+ for (const entry of block.connections ?? []) {
311
+ const id = `mcp-connector:${entry.key}`;
312
+ // 双来源同名:告诉用户「这条连接和 patch 行里的同名 server 是两份」,不替用户做决定。
313
+ // 放在「需重填」判断**之前**——两条事实都要讲,不能因为一条要重填就吞掉另一条。
314
+ if (data.servers.some((s) => s.serverName === entry.serverName)) {
315
+ items.push({
316
+ id: `mcp-connector-name:${entry.key}`,
317
+ kind: 'Warning', adapter: 'mcp',
318
+ description: msg('adapter.mcpConnectorDuplicateName', { serverName: entry.serverName }),
319
+ severity: 'warning',
320
+ });
321
+ }
322
+ const refill = refillFieldsOf(entry);
323
+ if (refill.length > 0) {
324
+ items.push({
325
+ id, kind: 'Warning', adapter: 'mcp',
326
+ description: msg('adapter.mcpConnectorNeedsRefill', { name: entry.name }),
327
+ detail: refill.join(', '),
328
+ severity: 'warning',
329
+ });
330
+ continue;
331
+ }
332
+ if (!writable)
333
+ continue;
334
+ const existing = findConnectorRecord(target.connections, entry);
335
+ const detail = describeConnector(entry);
336
+ if (existing === undefined) {
337
+ items.push({
338
+ id, kind: 'Create', adapter: 'mcp',
339
+ description: msg('adapter.mcpConnectorCreate', { name: entry.name }),
340
+ detail,
341
+ severity: 'info',
342
+ target: { adapter: 'mcp', ref: MCP_CONNECTOR_STORE_REL },
343
+ });
344
+ }
345
+ else if (isDeepStrictEqual(comparableConnector(existing), comparableConnector(entry))) {
346
+ items.push({ id, kind: 'Skip', adapter: 'mcp', description: msg('adapter.mcpConnectorSame', { name: entry.name }), severity: 'info' });
347
+ }
348
+ else {
349
+ items.push({
350
+ id, kind: 'Conflict', adapter: 'mcp',
351
+ description: msg('adapter.mcpConnectorDiff', { name: entry.name }),
352
+ detail: `current=${JSON.stringify(comparableConnector(existing))} imported=${JSON.stringify(comparableConnector(entry))}`.slice(0, 200),
353
+ severity: 'warning',
354
+ target: { adapter: 'mcp', ref: MCP_CONNECTOR_STORE_REL },
355
+ });
356
+ }
357
+ }
358
+ for (const oauth of block.oauthConnections ?? []) {
359
+ items.push({
360
+ id: `mcp-connector:${oauth.key}`, kind: 'Warning', adapter: 'mcp',
361
+ description: msg('adapter.mcpConnectorOauth', { name: oauth.name }),
362
+ severity: 'warning',
363
+ });
364
+ }
154
365
  return items;
155
366
  }
156
367
  async applyItem(item, ctx) {
368
+ if (item.id.startsWith(MCP_CONNECTOR_ITEM_PREFIX))
369
+ return this.applyConnectorItem(item, ctx);
157
370
  const msg = ctx.msg;
158
371
  const serverName = item.id.replace(/^mcp:/, '');
159
372
  const data = ctx.sections.get('mcp');
@@ -174,6 +387,39 @@ export class McpAdapter {
174
387
  ]);
175
388
  return { ok: true, needsRestart: true, message: msg('adapter.mcpWritten', { serverName }) };
176
389
  }
390
+ /**
391
+ * 写入一条连接:读全文件 → 只替换 `tables.connections[<key>]` → 整文件原子写。
392
+ *
393
+ * 为什么整文件写而不是走宿主 storage 服务:本机没装 `@deepseek-ai/dsh-storage-domain`,
394
+ * 我们拿不到 `mcp_connector` 域的表句柄;而直接改文件又会被运行中的宿主内存态覆盖 ⇒
395
+ * 计划项与本结果都显式要求重启(needsRestart),写之前先把原文件整体快照(引擎侧按
396
+ * `storages/` 前缀走文件类快照)。
397
+ */
398
+ async applyConnectorItem(item, ctx) {
399
+ const msg = ctx.msg;
400
+ const key = item.id.slice(MCP_CONNECTOR_ITEM_PREFIX.length);
401
+ const data = ctx.sections.get('mcp');
402
+ const entry = data?.connectors?.connections.find((c) => c.key === key);
403
+ if (entry === undefined)
404
+ return { ok: false, message: msg('adapter.mcpConnectorMissing', { name: key }) };
405
+ const refill = refillFieldsOf(entry);
406
+ if (refill.length > 0) {
407
+ return { ok: false, message: msg('adapter.mcpConnectorNeedsRefill', { name: entry.name }) };
408
+ }
409
+ const store = await readConnectorStore(ctx.target.fs);
410
+ if (store.exists && !store.parsed) {
411
+ return { ok: false, message: msg('adapter.mcpConnectorTargetUnreadable', { reason: store.error ?? 'unknown' }) };
412
+ }
413
+ const existing = findConnectorRecord(store.connections, entry);
414
+ const record = buildConnectorRecord(entry, {
415
+ existing,
416
+ key: pickConnectorKey(store.connections, entry),
417
+ now: Date.now(),
418
+ });
419
+ const text = mergeConnectorIntoStoreText(store, record);
420
+ await ctx.target.fs.writeFile(MCP_CONNECTOR_STORE_REL, new TextEncoder().encode(text));
421
+ return { ok: true, needsRestart: true, message: msg('adapter.mcpConnectorWritten', { name: entry.name }) };
422
+ }
177
423
  async validate(data, msg = zhMsg) {
178
424
  return validateJsonSection('mcp', data, msg, (section, issues) => {
179
425
  if (!Array.isArray(section.servers)) {
@@ -186,6 +432,22 @@ export class McpAdapter {
186
432
  }
187
433
  }
188
434
  }
435
+ // connectors 是**可选追加字段**:旧备份没有它 = 合法;有它但形态不对才算错
436
+ const connectors = section.connectors;
437
+ if (connectors !== undefined) {
438
+ if (connectors === null || typeof connectors !== 'object' || !Array.isArray(connectors.connections)) {
439
+ issues.push({ path: 'connectors.connections', message: msg('adapter.validate.array', { subject: 'connectors.connections' }), severity: 'error' });
440
+ }
441
+ else {
442
+ for (const c of connectors.connections) {
443
+ if (c === null || typeof c !== 'object'
444
+ || typeof c.key !== 'string' || c.key === ''
445
+ || typeof c.serverName !== 'string' || c.serverName === '') {
446
+ issues.push({ path: 'connectors.connections[]', message: msg('adapter.mcpConnectorInvalid'), severity: 'error' });
447
+ }
448
+ }
449
+ }
450
+ }
189
451
  });
190
452
  }
191
453
  }
@@ -15,6 +15,9 @@ export declare const zh: {
15
15
  readonly 'adapter.pluginDataEnvOverridden': '{path} 的位置被环境变量 {env} 覆盖,本次已跳过(不猜它指向哪里)';
16
16
  readonly 'adapter.pluginDataPathNotAllowed': '路径不在第三方插件数据的允许范围内,已拒绝写入: {path}';
17
17
  readonly 'adapter.pluginDataNoOsHome': '第三方插件的数据在系统主目录下,但当前宿主没有提供该目录的文件通道(旧版本宿主):本次不读取也不导入 / third-party plugin data lives under the system home directory, but this host offers no file channel for it (older host): nothing is read or imported this time';
18
+ readonly 'adapter.pwMachinePathStripped': '已剥离 {count} 条与本机路径绑定的 pnpm 设置(换机后会指向不存在的目录):{keys}';
19
+ readonly 'adapter.pwMachinePathKept': '保留本机原有的 {count} 条 pnpm 路径设置(不搬运来源机的路径):{keys}';
20
+ readonly 'adapter.pwLocalReadFailed': '读取本机 pnpm-workspace.yaml 失败:{reason}(未写入 —— 避免把本机自己的 pnpm 路径设置清掉)';
18
21
  };
19
22
  export declare const en: Record<keyof typeof zh, string>;
20
23
  /** 本域活翻译器(与 core 的 engineMsg 同一实现;导入本模块即完成目录注册)。 */
@@ -33,6 +33,10 @@ export const zh = {
33
33
  'adapter.pluginDataEnvOverridden': '{path} 的位置被环境变量 {env} 覆盖,本次已跳过(不猜它指向哪里)',
34
34
  'adapter.pluginDataPathNotAllowed': '路径不在第三方插件数据的允许范围内,已拒绝写入: {path}',
35
35
  'adapter.pluginDataNoOsHome': '第三方插件的数据在系统主目录下,但当前宿主没有提供该目录的文件通道(旧版本宿主):本次不读取也不导入 / third-party plugin data lives under the system home directory, but this host offers no file channel for it (older host): nothing is read or imported this time',
36
+ // ---------- pnpm 配置可移植净化(机器相关路径键) ----------
37
+ 'adapter.pwMachinePathStripped': '已剥离 {count} 条与本机路径绑定的 pnpm 设置(换机后会指向不存在的目录):{keys}',
38
+ 'adapter.pwMachinePathKept': '保留本机原有的 {count} 条 pnpm 路径设置(不搬运来源机的路径):{keys}',
39
+ 'adapter.pwLocalReadFailed': '读取本机 pnpm-workspace.yaml 失败:{reason}(未写入 —— 避免把本机自己的 pnpm 路径设置清掉)',
36
40
  };
37
41
  export const en = {
38
42
  'adapter.linkDirEntry': '{path} ({files})',
@@ -50,6 +54,9 @@ export const en = {
50
54
  'adapter.pluginDataEnvOverridden': "the location of {path} is overridden by the {env} environment variable; skipped this time (the new location is never guessed)",
51
55
  'adapter.pluginDataPathNotAllowed': 'the path is outside the range allowed for third-party plugin data and was refused: {path}',
52
56
  'adapter.pluginDataNoOsHome': 'third-party plugin data lives under the system home directory, but this host offers no file channel for it (older host): nothing is read or imported this time',
57
+ 'adapter.pwMachinePathStripped': 'Stripped {count} pnpm setting(s) bound to machine-specific paths (they would point to non-existent directories on another machine): {keys}',
58
+ 'adapter.pwMachinePathKept': "Kept this machine's own {count} pnpm path setting(s) (the source machine's paths are never carried over): {keys}",
59
+ 'adapter.pwLocalReadFailed': "Failed to read the local pnpm-workspace.yaml: {reason} (nothing was written, so this machine's own pnpm path settings are not dropped)",
53
60
  };
54
61
  registerMessageCatalog({ id: 'adapters-domain', zh, en });
55
62
  /** 本域活翻译器(与 core 的 engineMsg 同一实现;导入本模块即完成目录注册)。 */
@@ -106,7 +106,8 @@ export class PluginFilesAdapter {
106
106
  }
107
107
  /** 单元清单(零 I/O):逐文件单元,id 与导入侧 `pluginFiles:<relPath>` 一致。 */
108
108
  listUnits(section) {
109
- return unitsFromFiles(this.id, section.data.files, (rel) => rel);
109
+ // files 可能缺省(空分区 / 手写 section):按空表处理,不抛错 —— 选择器对任何分区都会调 listUnits
110
+ return unitsFromFiles(this.id, section.data.files ?? [], (rel) => rel);
110
111
  }
111
112
  async analyzeImport(data, ctx) {
112
113
  const msg = ctx.msg;
@@ -72,6 +72,22 @@ export declare class PluginsAdapter implements ConfigAdapter<PluginsSection> {
72
72
  * 不做任何写入(写入在 applyItem 内、且先于 pnpm-workspace.yaml)。
73
73
  */
74
74
  private patchAvailability;
75
+ /**
76
+ * 读目标机 profile 的 `dsh.profile.bundles`(issue #95)。
77
+ *
78
+ * 相对路径与写入目标同源(`profiles/<profile>/package.json`),走 `ctx.target.fs` 而不是
79
+ * 宿主私有通道:与 pnpm-workspace.yaml / patch 文件同一条 I/O 纪律(受限、可被 MemFs 测试)。
80
+ */
81
+ private targetBundles;
82
+ /**
83
+ * 把 host 自带的层声明进目标机 profile 的 `dsh.profile.bundles`(issue #95)。
84
+ *
85
+ * 为什么**不走安装通道**:这类层的包由 DSH 从自己的安装位置解析;按裸包名 `dsh plugin add`
86
+ * 会把 npm `dist-tags.latest` 的副本装进 profile,与 host 的版本线脱节 —— 真机故障:
87
+ * host 0.2.1-rc.2 + 包 0.2.1-alpha.2 的隔离组契约对不上 → `session-controller` 一直 pending
88
+ * → 新建会话 service-unavailable。声明不了时只报非致命 warning(§34.17),不拖垮其余配置。
89
+ */
90
+ private declareHostBundle;
75
91
  analyzeImport(data: PluginsSection, ctx: ImportContext): Promise<PlanItem[]>;
76
92
  applyItem(item: PlanItem, ctx: ImportContext): Promise<ApplyResult>;
77
93
  validate(data: PluginsSection, msg?: MsgFunc): Promise<ValidationResult>;