dsh-config-manager 0.1.69 → 0.1.70

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 (719) hide show
  1. package/README.md +59 -18
  2. package/README.zh-CN.md +29 -9
  3. package/lib/adapters/credentials.js +6 -3
  4. package/lib/adapters/file-collection.d.ts +21 -1
  5. package/lib/adapters/file-collection.js +32 -8
  6. package/lib/adapters/mcp.d.ts +12 -2
  7. package/lib/adapters/mcp.js +30 -13
  8. package/lib/adapters/plugin-files.d.ts +8 -0
  9. package/lib/adapters/plugin-files.js +36 -4
  10. package/lib/adapters/plugins.d.ts +28 -2
  11. package/lib/adapters/plugins.js +204 -55
  12. package/lib/adapters/prompts.d.ts +12 -2
  13. package/lib/adapters/prompts.js +34 -19
  14. package/lib/adapters/providers.js +5 -4
  15. package/lib/adapters/self.d.ts +20 -0
  16. package/lib/adapters/self.js +73 -0
  17. package/lib/adapters/sessions.d.ts +8 -0
  18. package/lib/adapters/sessions.js +47 -1
  19. package/lib/adapters/settings.d.ts +21 -3
  20. package/lib/adapters/settings.js +44 -7
  21. package/lib/adapters/skills.d.ts +36 -4
  22. package/lib/adapters/skills.js +222 -3
  23. package/lib/adapters/test-helpers.d.ts +31 -2
  24. package/lib/adapters/test-helpers.js +42 -5
  25. package/lib/adapters/workspaces.js +97 -5
  26. package/lib/cli/actions.d.ts +50 -3
  27. package/lib/cli/actions.js +103 -11
  28. package/lib/cli/help.d.ts +65 -0
  29. package/lib/cli/help.js +517 -0
  30. package/lib/cli/import-source.d.ts +32 -0
  31. package/lib/cli/import-source.js +178 -0
  32. package/lib/cli/index.d.ts +18 -2
  33. package/lib/cli/index.js +118 -90
  34. package/lib/cli/web/client-script.d.ts +8 -0
  35. package/lib/cli/web/client-script.js +11 -0
  36. package/lib/cli/web/http.d.ts +5 -0
  37. package/lib/cli/web/http.js +2 -1
  38. package/lib/cli/web/page.d.ts +16 -6
  39. package/lib/cli/web/page.js +102 -25
  40. package/lib/cli/web/routes.js +24 -5
  41. package/lib/cli/web/server.js +23 -2
  42. package/lib/client.d.ts +1019 -36
  43. package/lib/client.js +5529 -1571
  44. package/lib/core/analyzer.d.ts +15 -1
  45. package/lib/core/analyzer.js +114 -13
  46. package/lib/core/backup-plan.js +17 -1
  47. package/lib/core/backup.d.ts +8 -0
  48. package/lib/core/backup.js +28 -6
  49. package/lib/core/cache-cleaner.d.ts +10 -0
  50. package/lib/core/cache-cleaner.js +63 -22
  51. package/lib/core/checkpoint.d.ts +362 -0
  52. package/lib/core/checkpoint.js +1309 -0
  53. package/lib/core/journal.d.ts +37 -0
  54. package/lib/core/journal.js +69 -0
  55. package/lib/core/messages.d.ts +13 -0
  56. package/lib/core/messages.js +26 -0
  57. package/lib/core/model-tools.js +3 -0
  58. package/lib/core/patch-layers.d.ts +83 -0
  59. package/lib/core/patch-layers.js +110 -0
  60. package/lib/core/restore.d.ts +21 -0
  61. package/lib/core/restore.js +37 -3
  62. package/lib/core/rollback.js +6 -3
  63. package/lib/core/self-update.d.ts +52 -0
  64. package/lib/core/self-update.js +70 -0
  65. package/lib/core/session-health.d.ts +29 -5
  66. package/lib/core/session-health.js +28 -3
  67. package/lib/core/types.d.ts +126 -0
  68. package/lib/foreign/antigravity.d.ts +28 -0
  69. package/lib/foreign/antigravity.js +157 -0
  70. package/lib/foreign/bundle.d.ts +25 -0
  71. package/lib/foreign/bundle.js +79 -0
  72. package/lib/foreign/chatgpt.d.ts +6 -0
  73. package/lib/foreign/chatgpt.js +25 -0
  74. package/lib/foreign/claude-code.d.ts +3 -0
  75. package/lib/foreign/claude-code.js +85 -0
  76. package/lib/foreign/claude-sessions.d.ts +100 -0
  77. package/lib/foreign/claude-sessions.js +291 -0
  78. package/lib/foreign/cline.d.ts +6 -0
  79. package/lib/foreign/cline.js +27 -0
  80. package/lib/foreign/codex.d.ts +83 -0
  81. package/lib/foreign/codex.js +348 -0
  82. package/lib/foreign/continue.d.ts +9 -0
  83. package/lib/foreign/continue.js +65 -0
  84. package/lib/foreign/copilot.d.ts +39 -0
  85. package/lib/foreign/copilot.js +95 -0
  86. package/lib/foreign/crush.d.ts +11 -0
  87. package/lib/foreign/crush.js +57 -0
  88. package/lib/foreign/cursor.d.ts +87 -0
  89. package/lib/foreign/cursor.js +171 -0
  90. package/lib/foreign/dsh.d.ts +30 -0
  91. package/lib/foreign/dsh.js +101 -0
  92. package/lib/foreign/dsh4.d.ts +3 -0
  93. package/lib/foreign/dsh4.js +13 -0
  94. package/lib/foreign/gemini.d.ts +7 -0
  95. package/lib/foreign/gemini.js +44 -0
  96. package/lib/foreign/goose.d.ts +11 -0
  97. package/lib/foreign/goose.js +53 -0
  98. package/lib/foreign/grokbuild.d.ts +8 -0
  99. package/lib/foreign/grokbuild.js +25 -0
  100. package/lib/foreign/hermes.d.ts +2 -0
  101. package/lib/foreign/hermes.js +78 -0
  102. package/lib/foreign/kernel.d.ts +132 -0
  103. package/lib/foreign/kernel.js +569 -0
  104. package/lib/foreign/kilocode.d.ts +11 -0
  105. package/lib/foreign/kilocode.js +51 -0
  106. package/lib/foreign/kimi.d.ts +7 -0
  107. package/lib/foreign/kimi.js +47 -0
  108. package/lib/foreign/mimocode.d.ts +11 -0
  109. package/lib/foreign/mimocode.js +51 -0
  110. package/lib/foreign/openclaw.d.ts +7 -0
  111. package/lib/foreign/openclaw.js +46 -0
  112. package/lib/foreign/opencode.d.ts +11 -0
  113. package/lib/foreign/opencode.js +52 -0
  114. package/lib/foreign/pi.d.ts +7 -0
  115. package/lib/foreign/pi.js +48 -0
  116. package/lib/foreign/platform-paths.d.ts +66 -0
  117. package/lib/foreign/platform-paths.js +141 -0
  118. package/lib/foreign/qoder.d.ts +10 -0
  119. package/lib/foreign/qoder.js +49 -0
  120. package/lib/foreign/qwen.d.ts +7 -0
  121. package/lib/foreign/qwen.js +45 -0
  122. package/lib/foreign/read-antigravity.d.ts +57 -0
  123. package/lib/foreign/read-antigravity.js +504 -0
  124. package/lib/foreign/read-chatgpt.d.ts +20 -0
  125. package/lib/foreign/read-chatgpt.js +509 -0
  126. package/lib/foreign/read-claude-code.d.ts +24 -0
  127. package/lib/foreign/read-claude-code.js +234 -0
  128. package/lib/foreign/read-cline.d.ts +53 -0
  129. package/lib/foreign/read-cline.js +479 -0
  130. package/lib/foreign/read-codex.d.ts +75 -0
  131. package/lib/foreign/read-codex.js +842 -0
  132. package/lib/foreign/read-continue.d.ts +87 -0
  133. package/lib/foreign/read-continue.js +489 -0
  134. package/lib/foreign/read-copilot.d.ts +42 -0
  135. package/lib/foreign/read-copilot.js +267 -0
  136. package/lib/foreign/read-crush.d.ts +54 -0
  137. package/lib/foreign/read-crush.js +478 -0
  138. package/lib/foreign/read-cursor.d.ts +141 -0
  139. package/lib/foreign/read-cursor.js +705 -0
  140. package/lib/foreign/read-dsh.d.ts +43 -0
  141. package/lib/foreign/read-dsh.js +205 -0
  142. package/lib/foreign/read-dsh4.d.ts +14 -0
  143. package/lib/foreign/read-dsh4.js +27 -0
  144. package/lib/foreign/read-gemini.d.ts +55 -0
  145. package/lib/foreign/read-gemini.js +273 -0
  146. package/lib/foreign/read-goose.d.ts +25 -0
  147. package/lib/foreign/read-goose.js +322 -0
  148. package/lib/foreign/read-grokbuild.d.ts +34 -0
  149. package/lib/foreign/read-grokbuild.js +455 -0
  150. package/lib/foreign/read-hermes.d.ts +74 -0
  151. package/lib/foreign/read-hermes.js +690 -0
  152. package/lib/foreign/read-kilocode.d.ts +17 -0
  153. package/lib/foreign/read-kilocode.js +46 -0
  154. package/lib/foreign/read-kimi.d.ts +63 -0
  155. package/lib/foreign/read-kimi.js +719 -0
  156. package/lib/foreign/read-mimocode.d.ts +12 -0
  157. package/lib/foreign/read-mimocode.js +56 -0
  158. package/lib/foreign/read-openclaw.d.ts +51 -0
  159. package/lib/foreign/read-openclaw.js +445 -0
  160. package/lib/foreign/read-opencode.d.ts +150 -0
  161. package/lib/foreign/read-opencode.js +896 -0
  162. package/lib/foreign/read-pi.d.ts +56 -0
  163. package/lib/foreign/read-pi.js +411 -0
  164. package/lib/foreign/read-qoder.d.ts +54 -0
  165. package/lib/foreign/read-qoder.js +317 -0
  166. package/lib/foreign/read-qwen.d.ts +40 -0
  167. package/lib/foreign/read-qwen.js +326 -0
  168. package/lib/foreign/read-reasonix.d.ts +66 -0
  169. package/lib/foreign/read-reasonix.js +537 -0
  170. package/lib/foreign/read-teleagent.d.ts +15 -0
  171. package/lib/foreign/read-teleagent.js +68 -0
  172. package/lib/foreign/read-trae.d.ts +67 -0
  173. package/lib/foreign/read-trae.js +492 -0
  174. package/lib/foreign/read-vibe.d.ts +28 -0
  175. package/lib/foreign/read-vibe.js +364 -0
  176. package/lib/foreign/read-workbuddy.d.ts +49 -0
  177. package/lib/foreign/read-workbuddy.js +346 -0
  178. package/lib/foreign/read-zcode.d.ts +12 -0
  179. package/lib/foreign/read-zcode.js +38 -0
  180. package/lib/foreign/read-zed.d.ts +30 -0
  181. package/lib/foreign/read-zed.js +473 -0
  182. package/lib/foreign/reasonix.d.ts +6 -0
  183. package/lib/foreign/reasonix.js +20 -0
  184. package/lib/foreign/registry.d.ts +196 -0
  185. package/lib/foreign/registry.js +573 -0
  186. package/lib/foreign/session-ir.d.ts +186 -0
  187. package/lib/foreign/session-ir.js +391 -0
  188. package/lib/foreign/session-read.d.ts +102 -0
  189. package/lib/foreign/session-read.js +254 -0
  190. package/lib/foreign/session-source.d.ts +216 -0
  191. package/lib/foreign/session-source.js +453 -0
  192. package/lib/foreign/source-modules.d.ts +45 -0
  193. package/lib/foreign/source-modules.js +52 -0
  194. package/lib/foreign/sqlite.d.ts +61 -0
  195. package/lib/foreign/sqlite.js +404 -0
  196. package/lib/foreign/teleagent.d.ts +11 -0
  197. package/lib/foreign/teleagent.js +53 -0
  198. package/lib/foreign/trae.d.ts +6 -0
  199. package/lib/foreign/trae.js +20 -0
  200. package/lib/foreign/truth-table.d.ts +62 -0
  201. package/lib/foreign/truth-table.js +333 -0
  202. package/lib/foreign/types.d.ts +272 -0
  203. package/lib/foreign/types.js +2 -0
  204. package/lib/foreign/vibe.d.ts +6 -0
  205. package/lib/foreign/vibe.js +23 -0
  206. package/lib/foreign/workbuddy.d.ts +7 -0
  207. package/lib/foreign/workbuddy.js +45 -0
  208. package/lib/foreign/zcode.d.ts +11 -0
  209. package/lib/foreign/zcode.js +51 -0
  210. package/lib/foreign/zed.d.ts +11 -0
  211. package/lib/foreign/zed.js +53 -0
  212. package/lib/index.d.ts +154 -13
  213. package/lib/index.js +661 -84
  214. package/lib/market/github-repos.d.ts +28 -0
  215. package/lib/market/github-repos.js +43 -1
  216. package/lib/market/types.d.ts +1 -3
  217. package/lib/profiles/dsh-profile-manager.d.ts +54 -2
  218. package/lib/profiles/dsh-profile-manager.js +182 -11
  219. package/lib/profiles/dsh-profile-shared.d.ts +47 -0
  220. package/lib/profiles/dsh-profile-shared.js +15 -1
  221. package/lib/profiles/index.d.ts +1 -1
  222. package/lib/profiles/index.js +1 -1
  223. package/lib/routes/consult.js +51 -5
  224. package/lib/routes/foreign-context.d.ts +43 -0
  225. package/lib/routes/foreign-context.js +65 -0
  226. package/lib/routes/foreign.d.ts +11 -0
  227. package/lib/routes/foreign.js +208 -0
  228. package/lib/routes/import.d.ts +8 -0
  229. package/lib/routes/import.js +22 -1
  230. package/lib/routes/index.d.ts +2 -2
  231. package/lib/routes/index.js +4 -0
  232. package/lib/routes/kit.d.ts +4 -3
  233. package/lib/routes/market.js +0 -1
  234. package/lib/routes/prefs.d.ts +2 -1
  235. package/lib/routes/prefs.js +14 -2
  236. package/lib/routes/recovery.js +363 -3
  237. package/lib/routes/session-export.d.ts +117 -0
  238. package/lib/routes/session-export.js +270 -0
  239. package/lib/routes/snapshots.js +12 -2
  240. package/lib/routes/sync.js +82 -10
  241. package/lib/security/redaction.d.ts +14 -0
  242. package/lib/security/redaction.js +24 -9
  243. package/lib/security/secret-scanner.d.ts +27 -0
  244. package/lib/security/secret-scanner.js +54 -1
  245. package/lib/sync/autosync-scheduler.d.ts +13 -1
  246. package/lib/sync/autosync-scheduler.js +49 -26
  247. package/lib/sync/backup-files.js +6 -0
  248. package/lib/sync/gist/gist-transport.d.ts +91 -0
  249. package/lib/sync/gist/gist-transport.js +406 -0
  250. package/lib/sync/gist/messages.d.ts +22 -0
  251. package/lib/sync/gist/messages.js +46 -0
  252. package/lib/sync/s3/messages.d.ts +53 -0
  253. package/lib/sync/s3/messages.js +83 -0
  254. package/lib/sync/s3/s3-providers.d.ts +78 -0
  255. package/lib/sync/s3/s3-providers.js +156 -0
  256. package/lib/sync/s3/s3-transport.d.ts +126 -0
  257. package/lib/sync/s3/s3-transport.js +551 -0
  258. package/lib/sync/s3/sigv4.d.ts +107 -0
  259. package/lib/sync/s3/sigv4.js +196 -0
  260. package/lib/sync/sync-config.d.ts +250 -4
  261. package/lib/sync/sync-config.js +658 -57
  262. package/lib/sync/sync-engine.d.ts +1 -0
  263. package/lib/sync/sync-engine.js +4 -2
  264. package/lib/sync/sync-history.d.ts +1 -1
  265. package/lib/sync/webdav/webdav-transport.d.ts +31 -3
  266. package/lib/sync/webdav/webdav-transport.js +102 -18
  267. package/lib/ui/commands.d.ts +4 -3
  268. package/lib/ui/commands.js +41 -9
  269. package/lib/ui/disk-usage-view.d.ts +44 -0
  270. package/lib/ui/disk-usage-view.js +55 -1
  271. package/lib/ui/dsh-profiles-view.d.ts +53 -0
  272. package/lib/ui/dsh-profiles-view.js +56 -0
  273. package/lib/ui/foreign-view.d.ts +162 -0
  274. package/lib/ui/foreign-view.js +225 -0
  275. package/lib/ui/i18n.d.ts +77 -0
  276. package/lib/ui/i18n.js +164 -0
  277. package/lib/ui/session-inventory-view.d.ts +106 -0
  278. package/lib/ui/session-inventory-view.js +58 -0
  279. package/lib/ui/session-layout-view.d.ts +265 -0
  280. package/lib/ui/session-layout-view.js +271 -0
  281. package/lib/ui/sync-repository-picker.d.ts +98 -0
  282. package/lib/ui/sync-repository-picker.js +145 -0
  283. package/lib/ui/sync-settings-view.d.ts +143 -4
  284. package/lib/ui/sync-settings-view.js +231 -7
  285. package/lib/ui/types.d.ts +47 -0
  286. package/lib/utils/asar-read.d.ts +113 -0
  287. package/lib/utils/asar-read.js +523 -0
  288. package/lib/utils/env-lock.js +7 -2
  289. package/lib/utils/patch-yaml.d.ts +25 -0
  290. package/lib/utils/patch-yaml.js +62 -0
  291. package/lib/utils/paths.d.ts +10 -1
  292. package/lib/utils/paths.js +24 -3
  293. package/lib/utils/recursive-walk.js +12 -13
  294. package/lib/utils/session-format.d.ts +35 -2
  295. package/lib/utils/session-format.js +131 -5
  296. package/lib/utils/session-health-scan.d.ts +7 -1
  297. package/lib/utils/session-health-scan.js +369 -12
  298. package/lib/utils/session-layout-repair-service.d.ts +98 -0
  299. package/lib/utils/session-layout-repair-service.js +457 -0
  300. package/lib/utils/session-log-repair.d.ts +27 -0
  301. package/lib/utils/session-log-repair.js +233 -62
  302. package/lib/utils/session-repair-service.d.ts +32 -3
  303. package/lib/utils/session-repair-service.js +148 -9
  304. package/lib/utils/session-row-facts.d.ts +73 -0
  305. package/lib/utils/session-row-facts.js +182 -0
  306. package/lib/utils/session-verify.d.ts +118 -0
  307. package/lib/utils/session-verify.js +705 -0
  308. package/lib/utils/shared-constants.d.ts +59 -0
  309. package/lib/utils/shared-constants.js +71 -0
  310. package/package.json +1 -1
  311. package/src/adapters/credentials.test.ts +27 -0
  312. package/src/adapters/credentials.ts +6 -3
  313. package/src/adapters/file-collection.ts +33 -8
  314. package/src/adapters/files.test.ts +222 -0
  315. package/src/adapters/mcp.test.ts +43 -0
  316. package/src/adapters/mcp.ts +37 -14
  317. package/src/adapters/plugin-files.ts +34 -4
  318. package/src/adapters/plugins.test.ts +241 -0
  319. package/src/adapters/plugins.ts +201 -53
  320. package/src/adapters/prompts.test.ts +49 -0
  321. package/src/adapters/prompts.ts +41 -20
  322. package/src/adapters/providers.test.ts +16 -0
  323. package/src/adapters/providers.ts +4 -3
  324. package/src/adapters/self.test.ts +77 -0
  325. package/src/adapters/self.ts +75 -0
  326. package/src/adapters/sessions.test.ts +77 -0
  327. package/src/adapters/sessions.ts +47 -1
  328. package/src/adapters/settings.test.ts +78 -0
  329. package/src/adapters/settings.ts +42 -6
  330. package/src/adapters/skills.ts +221 -3
  331. package/src/adapters/test-helpers.ts +43 -5
  332. package/src/adapters/workspaces-dup-id.test.ts +144 -0
  333. package/src/adapters/workspaces.test.ts +10 -0
  334. package/src/adapters/workspaces.ts +97 -5
  335. package/src/cli/actions.test.ts +117 -2
  336. package/src/cli/actions.ts +132 -12
  337. package/src/cli/help.ts +570 -0
  338. package/src/cli/import-source.test.ts +193 -0
  339. package/src/cli/import-source.ts +229 -0
  340. package/src/cli/index.ts +129 -94
  341. package/src/cli/web/client-script.ts +12 -0
  342. package/src/cli/web/console-fixes.test.ts +36 -0
  343. package/src/cli/web/http.ts +7 -1
  344. package/src/cli/web/page.ts +108 -25
  345. package/src/cli/web/routes.ts +23 -4
  346. package/src/cli/web/script-anchor.test.ts +33 -0
  347. package/src/cli/web/server.ts +28 -2
  348. package/src/cli/web/web.test.ts +447 -4
  349. package/src/client/ConfigManagerSection.tsx +118 -63
  350. package/src/client/about/AboutPanel.tsx +42 -3
  351. package/src/client/about/PluginUpdateDialog.tsx +185 -0
  352. package/src/client/about/ReleaseBody.tsx +144 -0
  353. package/src/client/about/ReleaseNotesDialog.tsx +3 -130
  354. package/src/client/about/about-view.ts +7 -5
  355. package/src/client/about/plugin-update-view.test.ts +25 -0
  356. package/src/client/about/plugin-update-view.ts +26 -0
  357. package/src/client/about/release-notes-view.test.ts +33 -0
  358. package/src/client/about/release-notes-view.ts +34 -0
  359. package/src/client/api.ts +145 -1
  360. package/src/client/common/Icon.tsx +3 -0
  361. package/src/client/common/RunsCenter.tsx +8 -2
  362. package/src/client/common/Select.tsx +9 -2
  363. package/src/client/common/info-hint-guard.test.ts +63 -18
  364. package/src/client/common/plan-text-redaction.test.ts +193 -8
  365. package/src/client/common/routes.ts +15 -0
  366. package/src/client/config-manager.module.css +506 -354
  367. package/src/client/css-scale-guard.test.ts +122 -0
  368. package/src/client/css-token-guard.test.ts +82 -0
  369. package/src/client/environment/EnvironmentPanel.tsx +45 -16
  370. package/src/client/{snapshots → environment/maintenance}/DiskUsageCard.tsx +21 -17
  371. package/src/client/environment/profiles-incomplete-ui.test.ts +103 -0
  372. package/src/client/export/ExportView.tsx +9 -2
  373. package/src/client/{snapshots → home}/BackupScheduleCard.tsx +13 -4
  374. package/src/client/home/HomePanel.tsx +69 -157
  375. package/src/client/icon-layer-guard.test.ts +1 -1
  376. package/src/client/import/ForeignImportView.tsx +318 -0
  377. package/src/client/import/ImportWizardView.tsx +3 -1
  378. package/src/client/import/foreign-import-view-guard.test.ts +195 -0
  379. package/src/client/import/import-file-select.test.ts +6 -6
  380. package/src/client/import/import-wizard-steps.tsx +17 -1
  381. package/src/client/import/use-import-wizard-controller.ts +3 -3
  382. package/src/client/index.ts +1 -1
  383. package/src/client/library/LibraryActions.tsx +31 -34
  384. package/src/client/library/LibraryPanel.tsx +4 -1
  385. package/src/client/{snapshots → library}/RestorePlanView.tsx +1 -1
  386. package/src/client/{snapshots → library}/files-layout.test.ts +2 -2
  387. package/src/client/{snapshots → library}/report-dismiss.test.ts +26 -3
  388. package/src/client/library/task-panel-visibility.test.ts +25 -17
  389. package/src/client/locales.ts +278 -36
  390. package/src/client/lucide-icons.d.ts +1 -0
  391. package/src/client/market/MarketPanel.tsx +11 -2
  392. package/src/client/recovery/IncompleteCopiesSection.tsx +132 -0
  393. package/src/client/recovery/RecoveryPanel.tsx +486 -59
  394. package/src/client/recovery/recovery-api.ts +30 -0
  395. package/src/client/recovery/recovery-incomplete-copies.test.ts +151 -0
  396. package/src/client/recovery/recovery-locales.test.ts +88 -0
  397. package/src/client/recovery/recovery-locales.ts +321 -6
  398. package/src/client/recovery/recovery-panel-layout.test.ts +52 -0
  399. package/src/client/recovery/recovery-view.ts +92 -2
  400. package/src/client/run-store.test.ts +201 -89
  401. package/src/client/run-store.ts +114 -143
  402. package/src/client/session-export/session-export-view.test.ts +103 -0
  403. package/src/client/session-export/session-export-view.ts +118 -0
  404. package/src/client/sync/ChannelConfigDialog.tsx +219 -14
  405. package/src/client/sync/SyncChannelEntryCard.tsx +52 -4
  406. package/src/client/sync/SyncLogList.tsx +1 -1
  407. package/src/client/sync/SyncPanel.tsx +166 -102
  408. package/src/client/sync/SyncRepositoryPicker.tsx +216 -0
  409. package/src/client/sync/SyncSettingsView.test.ts +6 -1
  410. package/src/client/sync/history-model.test.ts +94 -57
  411. package/src/client/sync/history-model.ts +44 -25
  412. package/src/client/sync/sync-api.test.ts +56 -0
  413. package/src/client/sync/sync-api.ts +92 -0
  414. package/src/client/sync/sync-locales.ts +162 -2
  415. package/src/client/sync/sync-view.test.ts +3 -8
  416. package/src/client/sync/sync-view.ts +111 -21
  417. package/src/core/analyzer-cache.test.ts +47 -0
  418. package/src/core/analyzer.ts +115 -14
  419. package/src/core/backup-plan.test.ts +35 -0
  420. package/src/core/backup-plan.ts +19 -1
  421. package/src/core/backup.test.ts +37 -1
  422. package/src/core/backup.ts +27 -6
  423. package/src/core/cache-cleaner.test.ts +120 -1
  424. package/src/core/cache-cleaner.ts +59 -22
  425. package/src/core/checkpoint.test.ts +665 -0
  426. package/src/core/checkpoint.ts +1614 -0
  427. package/src/core/credential-plan-consistency.test.ts +93 -0
  428. package/src/core/incident-wiring.test.ts +6 -2
  429. package/src/core/journal.test.ts +70 -0
  430. package/src/core/journal.ts +68 -0
  431. package/src/core/messages.ts +26 -0
  432. package/src/core/model-tools.ts +3 -0
  433. package/src/core/patch-layers.test.ts +94 -0
  434. package/src/core/patch-layers.ts +150 -0
  435. package/src/core/restore.ts +59 -3
  436. package/src/core/rollback.ts +6 -3
  437. package/src/core/section-content-consistency.test.ts +185 -0
  438. package/src/core/self-update.test.ts +110 -0
  439. package/src/core/self-update.ts +118 -0
  440. package/src/core/session-health.test.ts +91 -1
  441. package/src/core/session-health.ts +56 -9
  442. package/src/core/types.ts +127 -1
  443. package/src/foreign/antigravity.test.ts +271 -0
  444. package/src/foreign/antigravity.ts +188 -0
  445. package/src/foreign/bundle.ts +108 -0
  446. package/src/foreign/chatgpt.test.ts +279 -0
  447. package/src/foreign/chatgpt.ts +30 -0
  448. package/src/foreign/claude-code.test.ts +234 -0
  449. package/src/foreign/claude-code.ts +106 -0
  450. package/src/foreign/claude-sessions-bytes.test.ts +214 -0
  451. package/src/foreign/claude-sessions.test.ts +155 -0
  452. package/src/foreign/claude-sessions.ts +453 -0
  453. package/src/foreign/cline.test.ts +274 -0
  454. package/src/foreign/cline.ts +32 -0
  455. package/src/foreign/codex.test.ts +585 -0
  456. package/src/foreign/codex.ts +409 -0
  457. package/src/foreign/continue.test.ts +365 -0
  458. package/src/foreign/continue.ts +73 -0
  459. package/src/foreign/copilot.test.ts +273 -0
  460. package/src/foreign/copilot.ts +133 -0
  461. package/src/foreign/crush.test.ts +234 -0
  462. package/src/foreign/crush.ts +66 -0
  463. package/src/foreign/cursor.test.ts +509 -0
  464. package/src/foreign/cursor.ts +259 -0
  465. package/src/foreign/dsh.test.ts +198 -0
  466. package/src/foreign/dsh.ts +117 -0
  467. package/src/foreign/dsh4.test.ts +127 -0
  468. package/src/foreign/dsh4.ts +15 -0
  469. package/src/foreign/file-budget.test.ts +256 -0
  470. package/src/foreign/fixtures/antigravity/README.md +18 -0
  471. package/src/foreign/fixtures/antigravity/basic/gemini/antigravity/mcp_config.json +13 -0
  472. package/src/foreign/fixtures/antigravity/basic/gemini/config/mcp_config.json +24 -0
  473. package/src/foreign/fixtures/antigravity/empty/gemini/antigravity/mcp_config.json +0 -0
  474. package/src/foreign/fixtures/antigravity/empty/gemini/antigravity/mcp_oauth_tokens.json +0 -0
  475. package/src/foreign/fixtures/antigravity/empty/gemini/config/mcp_config.json +0 -0
  476. package/src/foreign/fixtures/codex/README.md +31 -0
  477. package/src/foreign/fixtures/codex/basic/.agents/skills/hello/SKILL.md +6 -0
  478. package/src/foreign/fixtures/codex/basic/.agents/skills/wrapper/nested/SKILL.md +6 -0
  479. package/src/foreign/fixtures/codex/basic/.codex/AGENTS.md +4 -0
  480. package/src/foreign/fixtures/codex/basic/.codex/config.toml +28 -0
  481. package/src/foreign/fixtures/codex/malformed/.codex/config.toml +3 -0
  482. package/src/foreign/fixtures/codex/override/.codex/AGENTS.md +1 -0
  483. package/src/foreign/fixtures/codex/override/.codex/AGENTS.override.md +3 -0
  484. package/src/foreign/fixtures/codex/sessions/.codex/archived_sessions/rollout-2026-10-05T09-00-00-22222222-2222-4222-8222-222222222222.jsonl +2 -0
  485. package/src/foreign/fixtures/codex/sessions/.codex/sessions/2026/10/06/rollout-2026-10-06T10-00-00-11111111-1111-4111-8111-111111111111.jsonl +15 -0
  486. package/src/foreign/fixtures/codex/sessions/.codex/sessions/2026/10/06/rollout-2026-10-06T11-00-00-33333333-3333-4333-8333-333333333333.jsonl +2 -0
  487. package/src/foreign/fixtures/codex/sessions/.codex/sessions/2026/10/06/rollout-2026-10-06T11-30-00-55555555-5555-4555-8555-555555555555.jsonl +2 -0
  488. package/src/foreign/fixtures/codex/sessions/.codex/sessions/2026/10/06/rollout-2026-10-06T12-00-00-44444444-4444-4444-8444-444444444444.jsonl +2 -0
  489. package/src/foreign/fixtures/codex/sessions/.codex/sessions/notes.jsonl +1 -0
  490. package/src/foreign/fixtures/copilot/README.md +22 -0
  491. package/src/foreign/fixtures/copilot/basic/copilot-instructions.md +3 -0
  492. package/src/foreign/fixtures/copilot/basic/instructions/empty.instructions.md +1 -0
  493. package/src/foreign/fixtures/copilot/basic/instructions/repo.instructions.md +5 -0
  494. package/src/foreign/fixtures/copilot/basic/mcp-config.json +24 -0
  495. package/src/foreign/fixtures/copilot/basic/skills/hello/SKILL.md +6 -0
  496. package/src/foreign/fixtures/copilot/overridden/mcp-config.json +10 -0
  497. package/src/foreign/fixtures/cursor/README.md +17 -0
  498. package/src/foreign/fixtures/cursor/alt/.cursor/mcp.json +5 -0
  499. package/src/foreign/fixtures/cursor/basic/.cursor/mcp.json +18 -0
  500. package/src/foreign/fixtures/cursor/basic/.cursor/rules/agent.mdc +5 -0
  501. package/src/foreign/fixtures/cursor/basic/.cursor/rules/always.mdc +6 -0
  502. package/src/foreign/fixtures/cursor/basic/.cursor/rules/auto.mdc +7 -0
  503. package/src/foreign/fixtures/cursor/basic/.cursor/rules/manual.mdc +3 -0
  504. package/src/foreign/fixtures/cursor/basic/.cursor/skills/.hidden/SKILL.md +6 -0
  505. package/src/foreign/fixtures/cursor/basic/.cursor/skills/hello/SKILL.md +6 -0
  506. package/src/foreign/fixtures/cursor/basic/.cursor/skills/wrapper/nested/SKILL.md +6 -0
  507. package/src/foreign/fixtures/cursor/basic/.cursor/skills/wrapper/nested/notes.md +1 -0
  508. package/src/foreign/fixtures/cursor/basic/.cursorrules +1 -0
  509. package/src/foreign/fixtures/cursor/basic/project/.cursor/mcp.json +9 -0
  510. package/src/foreign/fixtures/cursor/basic/project/.cursor/rules/project-always.mdc +5 -0
  511. package/src/foreign/fixtures/cursor/basic/project/.cursor/skills/hello/SKILL.md +6 -0
  512. package/src/foreign/fixtures/cursor/basic/project/.cursorrules +1 -0
  513. package/src/foreign/fixtures/cursor/empty/.cursor/mcp.json +0 -0
  514. package/src/foreign/fixtures/cursor/malformed/.cursor/mcp.json +3 -0
  515. package/src/foreign/fixtures/cursor/malformed/.cursor/rules/broken.mdc +6 -0
  516. package/src/foreign/fixtures/cursor/shape/.cursor/mcp.json +3 -0
  517. package/src/foreign/fixtures/hermes/README.md +14 -0
  518. package/src/foreign/fixtures/hermes/basic/SOUL.md +3 -0
  519. package/src/foreign/fixtures/hermes/basic/config.yaml +48 -0
  520. package/src/foreign/fixtures/hermes/basic/dotenv.fixture +1 -0
  521. package/src/foreign/fixtures/hermes/basic/memories/MEMORY.md +1 -0
  522. package/src/foreign/fixtures/hermes/basic/memories/USER.md +1 -0
  523. package/src/foreign/fixtures/hermes/basic/skills/se-team-design/SKILL.md +6 -0
  524. package/src/foreign/fixtures/hermes/basic/skills/se-team-design/examples/usage.md +1 -0
  525. package/src/foreign/fixtures/hermes/basic/skills/software-development/dogfood/SKILL.md +6 -0
  526. package/src/foreign/fixtures/hermes/basic/skills/software-development/dogfood/notes.md +1 -0
  527. package/src/foreign/fixtures/hermes/basic/state.db +1 -0
  528. package/src/foreign/fixtures/hermes/empty/config.yaml +0 -0
  529. package/src/foreign/gemini.test.ts +291 -0
  530. package/src/foreign/gemini.ts +52 -0
  531. package/src/foreign/goose.test.ts +196 -0
  532. package/src/foreign/goose.ts +62 -0
  533. package/src/foreign/grokbuild.test.ts +246 -0
  534. package/src/foreign/grokbuild.ts +30 -0
  535. package/src/foreign/hermes.test.ts +545 -0
  536. package/src/foreign/hermes.ts +86 -0
  537. package/src/foreign/kernel-credentials.test.ts +88 -0
  538. package/src/foreign/kernel-workspaces.test.ts +88 -0
  539. package/src/foreign/kernel.ts +584 -0
  540. package/src/foreign/kilocode.test.ts +186 -0
  541. package/src/foreign/kilocode.ts +60 -0
  542. package/src/foreign/kimi.test.ts +259 -0
  543. package/src/foreign/kimi.ts +55 -0
  544. package/src/foreign/mcp-value-shape.test.ts +286 -0
  545. package/src/foreign/mimocode.test.ts +178 -0
  546. package/src/foreign/mimocode.ts +60 -0
  547. package/src/foreign/openclaw.test.ts +248 -0
  548. package/src/foreign/openclaw.ts +54 -0
  549. package/src/foreign/opencode.test.ts +376 -0
  550. package/src/foreign/opencode.ts +61 -0
  551. package/src/foreign/pi.test.ts +275 -0
  552. package/src/foreign/pi.ts +56 -0
  553. package/src/foreign/platform-paths.ts +156 -0
  554. package/src/foreign/qoder.test.ts +266 -0
  555. package/src/foreign/qoder.ts +57 -0
  556. package/src/foreign/qwen.test.ts +203 -0
  557. package/src/foreign/qwen.ts +53 -0
  558. package/src/foreign/read-antigravity.ts +569 -0
  559. package/src/foreign/read-chatgpt.ts +506 -0
  560. package/src/foreign/read-claude-code.ts +264 -0
  561. package/src/foreign/read-cline.ts +517 -0
  562. package/src/foreign/read-codex.ts +823 -0
  563. package/src/foreign/read-continue.ts +557 -0
  564. package/src/foreign/read-copilot.ts +304 -0
  565. package/src/foreign/read-crush.ts +503 -0
  566. package/src/foreign/read-cursor.ts +778 -0
  567. package/src/foreign/read-dsh.ts +215 -0
  568. package/src/foreign/read-dsh4.ts +35 -0
  569. package/src/foreign/read-gemini.ts +306 -0
  570. package/src/foreign/read-goose.ts +333 -0
  571. package/src/foreign/read-grokbuild.ts +475 -0
  572. package/src/foreign/read-hermes.ts +724 -0
  573. package/src/foreign/read-kilocode.ts +55 -0
  574. package/src/foreign/read-kimi.ts +768 -0
  575. package/src/foreign/read-mimocode.ts +66 -0
  576. package/src/foreign/read-openclaw.ts +505 -0
  577. package/src/foreign/read-opencode.ts +994 -0
  578. package/src/foreign/read-pi.ts +459 -0
  579. package/src/foreign/read-qoder.ts +351 -0
  580. package/src/foreign/read-qwen.ts +342 -0
  581. package/src/foreign/read-reasonix.ts +590 -0
  582. package/src/foreign/read-teleagent.ts +82 -0
  583. package/src/foreign/read-trae.ts +488 -0
  584. package/src/foreign/read-vibe.ts +353 -0
  585. package/src/foreign/read-workbuddy.ts +374 -0
  586. package/src/foreign/read-zcode.ts +48 -0
  587. package/src/foreign/read-zed.ts +479 -0
  588. package/src/foreign/reasonix.test.ts +334 -0
  589. package/src/foreign/reasonix.ts +25 -0
  590. package/src/foreign/registry.test.ts +377 -0
  591. package/src/foreign/registry.ts +750 -0
  592. package/src/foreign/session-ir.test.ts +136 -0
  593. package/src/foreign/session-ir.ts +579 -0
  594. package/src/foreign/session-read.ts +295 -0
  595. package/src/foreign/session-source.ts +625 -0
  596. package/src/foreign/skill-file-caps.test.ts +171 -0
  597. package/src/foreign/source-modules.ts +86 -0
  598. package/src/foreign/source-registry.test.ts +190 -0
  599. package/src/foreign/sqlite.test.ts +313 -0
  600. package/src/foreign/sqlite.ts +446 -0
  601. package/src/foreign/teleagent.test.ts +147 -0
  602. package/src/foreign/teleagent.ts +62 -0
  603. package/src/foreign/trae.test.ts +316 -0
  604. package/src/foreign/trae.ts +25 -0
  605. package/src/foreign/truncation-report.test.ts +238 -0
  606. package/src/foreign/truth-table.ts +453 -0
  607. package/src/foreign/types.ts +312 -0
  608. package/src/foreign/vibe.test.ts +209 -0
  609. package/src/foreign/vibe.ts +28 -0
  610. package/src/foreign/workbuddy.test.ts +207 -0
  611. package/src/foreign/workbuddy.ts +53 -0
  612. package/src/foreign/zcode.test.ts +224 -0
  613. package/src/foreign/zcode.ts +60 -0
  614. package/src/foreign/zed.test.ts +240 -0
  615. package/src/foreign/zed.ts +62 -0
  616. package/src/index.facade.test.ts +254 -16
  617. package/src/index.ts +704 -82
  618. package/src/market/github-repos.test.ts +98 -1
  619. package/src/market/github-repos.ts +63 -1
  620. package/src/market/types.ts +3 -3
  621. package/src/profiles/dsh-profile-incomplete-copy.test.ts +177 -0
  622. package/src/profiles/dsh-profile-manager.test.ts +37 -0
  623. package/src/profiles/dsh-profile-manager.ts +188 -13
  624. package/src/profiles/dsh-profile-shared.ts +55 -1
  625. package/src/profiles/index.ts +2 -1
  626. package/src/routes/consult.ts +53 -4
  627. package/src/routes/foreign-context.ts +89 -0
  628. package/src/routes/foreign.test.ts +385 -0
  629. package/src/routes/foreign.ts +226 -0
  630. package/src/routes/import.ts +23 -1
  631. package/src/routes/index.ts +6 -2
  632. package/src/routes/kit.ts +4 -3
  633. package/src/routes/market.ts +0 -1
  634. package/src/routes/prefs.ts +15 -1
  635. package/src/routes/recovery.test.ts +167 -0
  636. package/src/routes/recovery.ts +331 -1
  637. package/src/routes/session-export.ts +308 -0
  638. package/src/routes/snapshots.ts +12 -2
  639. package/src/routes/sync.ts +91 -7
  640. package/src/security/redaction.test.ts +149 -0
  641. package/src/security/redaction.ts +25 -9
  642. package/src/security/secret-scanner.test.ts +186 -0
  643. package/src/security/secret-scanner.ts +57 -1
  644. package/src/sync/autosync-risk-tier.test.ts +61 -0
  645. package/src/sync/autosync-scheduler.test.ts +48 -1
  646. package/src/sync/autosync-scheduler.ts +50 -26
  647. package/src/sync/backup-files.ts +5 -0
  648. package/src/sync/gist/gist-msgkeys-catalog.test.ts +33 -0
  649. package/src/sync/gist/gist-msgkeys.test.ts +71 -0
  650. package/src/sync/gist/gist-transport.test.ts +308 -0
  651. package/src/sync/gist/gist-transport.ts +550 -0
  652. package/src/sync/gist/messages.ts +49 -0
  653. package/src/sync/retention-prune-all-off.test.ts +50 -0
  654. package/src/sync/s3/cloud-config.test.ts +283 -0
  655. package/src/sync/s3/messages.ts +102 -0
  656. package/src/sync/s3/s3-delete-index-404.test.ts +170 -0
  657. package/src/sync/s3/s3-delete-index-unreadable.test.ts +141 -0
  658. package/src/sync/s3/s3-msgkeys.test.ts +101 -0
  659. package/src/sync/s3/s3-providers.ts +213 -0
  660. package/src/sync/s3/s3-transport.test.ts +394 -0
  661. package/src/sync/s3/s3-transport.ts +651 -0
  662. package/src/sync/s3/s3-upload-index-404.test.ts +171 -0
  663. package/src/sync/s3/sigv4.test.ts +219 -0
  664. package/src/sync/s3/sigv4.ts +283 -0
  665. package/src/sync/sync-config.test.ts +237 -5
  666. package/src/sync/sync-config.ts +841 -52
  667. package/src/sync/sync-engine-download-tombstone.test.ts +73 -0
  668. package/src/sync/sync-engine.ts +5 -3
  669. package/src/sync/sync-history.ts +1 -1
  670. package/src/sync/webdav/webdav-delete-index-404.test.ts +178 -0
  671. package/src/sync/webdav/webdav-delete-index-unreadable.test.ts +143 -0
  672. package/src/sync/webdav/webdav-msgkeys.test.ts +53 -0
  673. package/src/sync/webdav/webdav-transport.test.ts +9 -5
  674. package/src/sync/webdav/webdav-transport.ts +99 -17
  675. package/src/sync/webdav/webdav-upload-index-404.test.ts +177 -0
  676. package/src/ui/commands.test.ts +4 -1
  677. package/src/ui/commands.ts +46 -12
  678. package/src/ui/disk-usage-view.test.ts +63 -1
  679. package/src/ui/disk-usage-view.ts +93 -1
  680. package/src/ui/dsh-profiles-incomplete-view.test.ts +102 -0
  681. package/src/ui/dsh-profiles-view.ts +94 -0
  682. package/src/ui/foreign-view.test.ts +241 -0
  683. package/src/ui/foreign-view.ts +362 -0
  684. package/src/ui/i18n.ts +164 -0
  685. package/src/ui/mid-ellipsis.test.ts +5 -10
  686. package/src/ui/session-inventory-view.test.ts +142 -2
  687. package/src/ui/session-inventory-view.ts +127 -0
  688. package/src/ui/session-layout-view.test.ts +361 -0
  689. package/src/ui/session-layout-view.ts +444 -0
  690. package/src/ui/sync-repository-picker.test.ts +130 -0
  691. package/src/ui/sync-repository-picker.ts +180 -0
  692. package/src/ui/sync-settings-view.test.ts +166 -0
  693. package/src/ui/sync-settings-view.ts +319 -9
  694. package/src/ui/types.ts +42 -0
  695. package/src/utils/asar-read.test.ts +330 -0
  696. package/src/utils/asar-read.ts +606 -0
  697. package/src/utils/env-lock.test.ts +55 -0
  698. package/src/utils/env-lock.ts +6 -1
  699. package/src/utils/patch-yaml.test.ts +112 -0
  700. package/src/utils/patch-yaml.ts +72 -0
  701. package/src/utils/paths.test.ts +63 -0
  702. package/src/utils/paths.ts +25 -3
  703. package/src/utils/recursive-walk.test.ts +20 -0
  704. package/src/utils/recursive-walk.ts +12 -13
  705. package/src/utils/session-format.test.ts +113 -2
  706. package/src/utils/session-format.ts +127 -5
  707. package/src/utils/session-health-scan.test.ts +364 -1
  708. package/src/utils/session-health-scan.ts +434 -15
  709. package/src/utils/session-layout-repair-service.test.ts +362 -0
  710. package/src/utils/session-layout-repair-service.ts +568 -0
  711. package/src/utils/session-log-repair.test.ts +260 -1
  712. package/src/utils/session-log-repair.ts +294 -55
  713. package/src/utils/session-repair-service.test.ts +148 -1
  714. package/src/utils/session-repair-service.ts +165 -10
  715. package/src/utils/session-row-facts.test.ts +159 -0
  716. package/src/utils/session-row-facts.ts +187 -0
  717. package/src/utils/session-verify.test.ts +762 -0
  718. package/src/utils/session-verify.ts +784 -0
  719. package/src/utils/shared-constants.ts +82 -0
@@ -45,8 +45,12 @@ export class PluginFilesAdapter {
45
45
  files.push({ relativePath: rel, data, contentHash: sha256Hex(data) });
46
46
  seen.add(rel);
47
47
  }
48
- catch {
49
- /* 白名单文件不存在则跳过 */
48
+ catch (err) {
49
+ // ui-F2:白名单文件**不存在**是正常形态(按需创建 → 跳过);**读不到**(权限/竞态/断链)
50
+ // 绝不能静默丢内容 —— 与文件集合基类同一语义:整分区显式失败,导出报告可见。
51
+ if (await this.absentOnDisk(ctx, rel))
52
+ continue;
53
+ throw err;
50
54
  }
51
55
  }
52
56
  // 2) 约定配置目录递归收集(相对 ~/.dsh 根的完整路径;与白名单文件去重)
@@ -69,8 +73,11 @@ export class PluginFilesAdapter {
69
73
  const data = await ctx.fs.readFile(rel);
70
74
  files.push({ relativePath: rel, data, contentHash: sha256Hex(data) });
71
75
  }
72
- catch {
73
- continue;
76
+ catch (err) {
77
+ // ui-F2:同上 —— 目录遍历刚点到的文件却读不到,是真实的读取失败,不是「不存在」。
78
+ if (await this.absentOnDisk(ctx, rel))
79
+ continue;
80
+ throw err;
74
81
  }
75
82
  }
76
83
  }
@@ -81,6 +88,21 @@ export class PluginFilesAdapter {
81
88
  warnings: linkWarnings(msgOf(ctx), this.displayName, listing),
82
89
  };
83
90
  }
91
+ /**
92
+ * 读失败之后判定「这个路径本来就不存在」还是「存在但读不到」(ui-F2)。
93
+ *
94
+ * 为什么不用错误码:门面错误的形状并不统一(内存 mock 与各平台 EACCES 文案不同),
95
+ * 而 `exists()` 是 FileSystemFacade 契约里唯一稳定的存在性判据。`exists()` 自己也失败时
96
+ * 按「存在」处理(保守:宁可让这次导出显式失败,也不静默丢一个文件)。
97
+ */
98
+ async absentOnDisk(ctx, rel) {
99
+ try {
100
+ return (await ctx.fs.exists(rel)) === false;
101
+ }
102
+ catch {
103
+ return false;
104
+ }
105
+ }
84
106
  /** 单元清单(零 I/O):逐文件单元,id 与导入侧 `pluginFiles:<relPath>` 一致。 */
85
107
  listUnits(section) {
86
108
  return unitsFromFiles(this.id, section.data.files, (rel) => rel);
@@ -144,6 +166,16 @@ export class PluginFilesAdapter {
144
166
  return validateJsonSection('pluginFiles', data, msg, (section, issues) => {
145
167
  if (!Array.isArray(section.files)) {
146
168
  issues.push({ path: 'files', message: msg('adapter.validate.array', { subject: 'files' }), severity: 'error' });
169
+ return;
170
+ }
171
+ // ui-F3:条目级形状校验必须与 analyzeImport 的假设对齐(它直接读 file.relativePath)。
172
+ // 只校验「是数组」会放过 files:[null] / relativePath:42 这类畸形,analyzeImport 抛内部
173
+ // TypeError → ImportAnalysis.valid=false → 整包被拦下,用户看到的是内部异常文本。
174
+ for (const [i, f] of section.files.entries()) {
175
+ const entry = f;
176
+ if (entry === null || typeof entry !== 'object' || typeof entry.relativePath !== 'string' || entry.relativePath === '') {
177
+ issues.push({ path: `files[${i}]`, message: msg('adapter.validate.fileRelativePath'), severity: 'error' });
178
+ }
147
179
  }
148
180
  });
149
181
  }
@@ -1,8 +1,13 @@
1
1
  import type { PackLocalPluginsResult } from '../core/local-plugin-pack.ts';
2
2
  import type { MsgFunc } from '../core/messages.ts';
3
+ import { USER_PATCH_FILE } from '../core/patch-layers.ts';
3
4
  import type { PluginEntry, PluginsSection } from '../schema/types.ts';
4
- import type { ApplyResult, ConfigAdapter, ExportOptions, ExportSection, ExportUnit, HostContext, ImportContext, PlanItem, ValidationResult } from '../core/types.ts';
5
- export declare const USER_PATCH_FILE = "cordis.patch.yml";
5
+ import type { ApplyResult, ConfigAdapter, ExportOptions, ExportSection, ExportUnit, HostContext, ImportContext, PlanItem, SectionPreview, ValidationResult } from '../core/types.ts';
6
+ /**
7
+ * 用户(home)patch 层路径 —— 定义已移到 `core/patch-layers.ts`(issue #71:patch 有 home /
8
+ * profile 两层,层身份属于引擎知识,不属于某个 adapter)。此处 re-export 保持既有 import 面。
9
+ */
10
+ export { USER_PATCH_FILE };
6
11
  /**
7
12
  * 本地源插件打包钩子(由宿主注入,见 src/index.ts createAdapters)。
8
13
  * 返回打包结果(含字节与重写后的 spec);不注入 = 不做本地源打包(保持旧行为)。
@@ -31,6 +36,27 @@ export declare class PluginsAdapter implements ConfigAdapter<PluginsSection> {
31
36
  private readonly localPack;
32
37
  constructor(selfName?: string, localPack?: LocalPluginPackHook);
33
38
  export(ctx: HostContext, options: ExportOptions): Promise<ExportSection<PluginsSection>>;
39
+ /**
40
+ * 只读预览(`/export-preview` 用):与 `export()` **同口径**,但不做任何本地源打包。
41
+ *
42
+ * 为什么必须覆写:基类缺省会让调用方退回 `export()`,而 `export()` 会为每个 `link:`/`file:`
43
+ * 插件 spawn 一次 `npm pack`(真机 12 个本地源插件 ≈12 s)。「总览」页只想知道条目数与体积,
44
+ * 却因此每次都要打包一遍全部本地插件 —— 预览是纯只读路径,不该有任何进程副作用。
45
+ *
46
+ * 与 `export()` 的**唯一**差异:
47
+ * - 不注入 `localTarballs`(没有字节),故清单 spec 保持原样(不做 `file:<tarball>` 改写);
48
+ * - 插件单元体积改为 `file:` 源的一次 stat(`measureLocalSources`),`link:` 目录按 0;
49
+ * - 体积用一次 `stringify` 算 UTF-8 字节数(与 /export-preview 对 JSON 分区的口径一致)。
50
+ * 其余(清单过滤、patch 行、pnpm-workspace、patchFiles、条目级白名单、告警文案、单元分组)逐字相同。
51
+ */
52
+ preview(ctx: HostContext, options: ExportOptions): Promise<SectionPreview<PluginsSection>>;
53
+ /**
54
+ * 导出与预览的共同内核。
55
+ *
56
+ * `pack=false` 时跳过本地源打包(预览路径)—— 除该段外两条路径逐字共用,
57
+ * 避免「预览说会带什么」与「导出真的带什么」分叉。
58
+ */
59
+ private collect;
34
60
  /**
35
61
  * 单元清单(Phase 1;零 I/O:直接由 export 产物派生)。
36
62
  *
@@ -10,17 +10,25 @@
10
10
  * 文件条目随分区进入 ZIP(落在 `plugin-files/local-plugins/` 前缀下,复用既有文件类分区通道,
11
11
  * **不新增分区 id**);导入时把 spec 重写为 `file:<解包后的绝对路径>` 再交给官方安装通道。
12
12
  */
13
+ import { join } from 'node:path';
13
14
  import { isDeepStrictEqual } from 'node:util';
14
15
  import { sha256Hex } from '../utils/hashing.js';
15
16
  import { installSpecFor, resolveProcessProfileName } from '../core/plugin-cli.js';
16
- import { LOCAL_PLUGIN_DIR } from '../core/local-plugin-pack.js';
17
+ import { classifyPluginSpec, LOCAL_PLUGIN_DIR, resolveLocalPluginPath } from '../core/local-plugin-pack.js';
17
18
  import { msgOf, zhMsg } from '../core/messages.js';
19
+ import { stringifyJsonSafe } from '../utils/json.js';
18
20
  import { isPathSafe, normalizePath } from '../utils/paths.js';
21
+ import { isENOENT } from '../utils/guards.js';
19
22
  import { PLUGIN_PATCH_REF_PREFIX } from '../core/backup.js';
23
+ import { readEffectivePatchLines, resolveWriteLayer, USER_PATCH_FILE } from '../core/patch-layers.js';
20
24
  import { parsePnpmPatchedDependencies, sanitizePnpmWorkspacePatches } from './pnpm-workspace.js';
21
25
  import { sectionMeta } from '../schema/section-registry.js';
22
26
  import { validateJsonSection } from './json-section.js';
23
- export const USER_PATCH_FILE = 'cordis.patch.yml';
27
+ /**
28
+ * 用户(home)patch 层路径 —— 定义已移到 `core/patch-layers.ts`(issue #71:patch 有 home /
29
+ * profile 两层,层身份属于引擎知识,不属于某个 adapter)。此处 re-export 保持既有 import 面。
30
+ */
31
+ export { USER_PATCH_FILE };
24
32
  /** tarball 字节 → base64(零依赖,避免 Buffer 在浏览器侧类型问题) */
25
33
  function bytesToBase64(data) {
26
34
  if (typeof Buffer !== 'undefined')
@@ -108,6 +116,109 @@ function entriesOfRaw(raw) {
108
116
  }
109
117
  return [];
110
118
  }
119
+ /**
120
+ * 预览路径测到的本地源插件体积(`preview()` 写入,`listUnits()` 读取)。
121
+ *
122
+ * 为什么用 WeakMap 而不是给 `PluginsSection` 加字段:`localTarballs` 是备份格式的一部分
123
+ * (schema 校验、市场安全防线都在看它),而预览产物里本就没有 tarball 字节;把「只读预览测到的
124
+ * 体积」混进载荷只会让「预览说的」与「导出做的」更难分辨。挂在载荷对象上则随对象一起回收,
125
+ * 导出产物(真实 base64)走另一条分支,行为逐字不变。
126
+ */
127
+ const previewTarballBytes = new WeakMap();
128
+ /**
129
+ * 零 spawn 推导本地源插件的体积(预览专用)。
130
+ *
131
+ * 两条路径,都是**只读度量**、都不 spawn 打包进程:
132
+ * - `file:` 源 → 一次 stat:源 tarball 本身就是 gzip 容器,字节数与 `npm pack` 产物同量级;
133
+ * - `link:` 源 → 目录体积上界(`fs.dirSizeBytes`):这是 `npm pack` 的输入目录,体积比
134
+ * pack 产物大(真机约 3.4 倍,pack 产物是 gzip),用来展示「量级」足够。
135
+ * 绝不在这里手写递归(见 local-plugin-pack.ts 安全约束:node_modules 由 npm pack 自己排除,
136
+ * 手写递归会把它整个算进来)——度量交给宿主门面,由它负责跳过 node_modules 与链接目录。
137
+ *
138
+ * 登记本身就是语义:Map 里有该插件 = 「该单元随备份携带一个文件」,与导出产物里
139
+ * `npm pack` 打出的 tarball(`fileCount: 1`)对齐;宿主未实现相应度量、越界路径
140
+ * (fs 门面对 home 外绝对路径抛 `host.fsPathEscape`)、目录读不到一律按 0 ——
141
+ * 预览绝不能因为一个体积数字而失败。
142
+ */
143
+ async function measureLocalSources(plugins, ctx) {
144
+ const sizes = new Map();
145
+ const statSize = ctx.fs.statSize;
146
+ const dirSize = ctx.fs.dirSizeBytes;
147
+ // profile 目录与打包钩子同源(local-plugin-host.ts:优先 profileDir,回退按 profile 名拼接)
148
+ const profileDir = ctx.profileDir !== undefined && ctx.profileDir !== ''
149
+ ? ctx.profileDir
150
+ : join(ctx.homeDir, PNPM_PROFILE_DIR(ctx.profile));
151
+ for (const pl of plugins) {
152
+ const kind = classifyPluginSpec(pl.spec);
153
+ if (kind !== 'file' && kind !== 'link')
154
+ continue;
155
+ try {
156
+ const abs = resolveLocalPluginPath(pl.spec ?? '', { homeDir: ctx.homeDir, profileDir });
157
+ // 宿主未实现对应度量 → null → 按 0(旧行为,只少一个数字)
158
+ const size = kind === 'link'
159
+ ? (dirSize !== undefined ? await dirSize.call(ctx.fs, abs) : null)
160
+ : (statSize !== undefined ? await statSize.call(ctx.fs, abs) : null);
161
+ sizes.set(pl.name, size !== null && size > 0 ? size : 0);
162
+ }
163
+ catch {
164
+ // 越界 / 读不到:按 0 处理(预览不因此失败),但仍算「随包携带一个文件」
165
+ sizes.set(pl.name, 0);
166
+ }
167
+ }
168
+ return sizes;
169
+ }
170
+ /**
171
+ * 由分区载荷派生单元清单(纯函数、零 I/O)。
172
+ *
173
+ * 单元 = 插件包 / cordis 补丁行 / pnpm-workspace.yaml / 单个 patch 文件。
174
+ * 后两者构成**原子组**:patchedDependencies 的声明与 patches/** 文件必须同进同出
175
+ * (issue #35 —— 只搬声明会让目标机 pnpm 拒绝一切 add),故互相写进 lockedWith,
176
+ * 由选择模型保证勾选/取消同步。
177
+ *
178
+ * `tarballBytes` = 预览路径登记到的本地源体积(见 measureLocalSources,0 = 体积未知但仍随包携带);
179
+ * 缺省时体积与文件数完全按导出产物里的 base64 计算 —— 与改造前逐字一致。
180
+ */
181
+ function unitsOf(data, tarballBytes) {
182
+ const wsId = 'plugins:pnpm-workspace';
183
+ const patchFileIds = (data.patchFiles ?? []).map((pf) => `plugins:patch:${normalizePath(pf.relativePath)}`);
184
+ const group = [wsId, ...patchFileIds];
185
+ const units = [];
186
+ for (const pl of data.plugins) {
187
+ const tarball = tarballOf(data, pl.name);
188
+ const measured = tarballBytes?.get(pl.name);
189
+ units.push({
190
+ id: `plugin:${pl.name}`,
191
+ label: pl.name,
192
+ detail: pl.version,
193
+ // base64 长度 → 原始字节数(仅用于展示体积,不求精确);预览路径没有字节,用 stat 量到的源体积
194
+ sizeBytes: tarball !== undefined ? Math.floor((tarball.base64.length * 3) / 4) : measured ?? 0,
195
+ ...(tarball !== undefined || measured !== undefined ? { fileCount: 1 } : {}),
196
+ });
197
+ }
198
+ for (const line of data.patch) {
199
+ // 由 mcp / prompts adapter 管理的行不属本分区(applyItem 同样跳过),列出来只会误导用户
200
+ if (isManagedElsewhere(line.raw))
201
+ continue;
202
+ units.push({ id: `patch:${line.lineId}`, label: line.lineId, sizeBytes: 0 });
203
+ }
204
+ if (data.pnpmWorkspace !== undefined && data.pnpmWorkspace !== null && data.pnpmWorkspace !== '') {
205
+ units.push({
206
+ id: wsId,
207
+ label: 'pnpm-workspace.yaml',
208
+ sizeBytes: data.pnpmWorkspace.length,
209
+ ...(group.length > 1 ? { lockedWith: group } : {}),
210
+ });
211
+ }
212
+ for (const pf of data.patchFiles ?? []) {
213
+ units.push({
214
+ id: `plugins:patch:${normalizePath(pf.relativePath)}`,
215
+ label: pf.relativePath,
216
+ sizeBytes: Math.floor((pf.base64.length * 3) / 4),
217
+ ...(group.length > 1 ? { lockedWith: group } : {}),
218
+ });
219
+ }
220
+ return units;
221
+ }
111
222
  export class PluginsAdapter {
112
223
  id = 'plugins';
113
224
  // 元数据唯一来源 = 注册表(t31):不再与 ui/export-flow.ts 的导出目录各写一份
@@ -123,6 +234,41 @@ export class PluginsAdapter {
123
234
  this.localPack = localPack;
124
235
  }
125
236
  async export(ctx, options) {
237
+ return this.collect(ctx, options, true);
238
+ }
239
+ /**
240
+ * 只读预览(`/export-preview` 用):与 `export()` **同口径**,但不做任何本地源打包。
241
+ *
242
+ * 为什么必须覆写:基类缺省会让调用方退回 `export()`,而 `export()` 会为每个 `link:`/`file:`
243
+ * 插件 spawn 一次 `npm pack`(真机 12 个本地源插件 ≈12 s)。「总览」页只想知道条目数与体积,
244
+ * 却因此每次都要打包一遍全部本地插件 —— 预览是纯只读路径,不该有任何进程副作用。
245
+ *
246
+ * 与 `export()` 的**唯一**差异:
247
+ * - 不注入 `localTarballs`(没有字节),故清单 spec 保持原样(不做 `file:<tarball>` 改写);
248
+ * - 插件单元体积改为 `file:` 源的一次 stat(`measureLocalSources`),`link:` 目录按 0;
249
+ * - 体积用一次 `stringify` 算 UTF-8 字节数(与 /export-preview 对 JSON 分区的口径一致)。
250
+ * 其余(清单过滤、patch 行、pnpm-workspace、patchFiles、条目级白名单、告警文案、单元分组)逐字相同。
251
+ */
252
+ async preview(ctx, options) {
253
+ const collected = await this.collect(ctx, options, false);
254
+ const data = collected.data;
255
+ const tarballBytes = await measureLocalSources(data.plugins, ctx);
256
+ if (tarballBytes.size > 0)
257
+ previewTarballBytes.set(data, tarballBytes);
258
+ return {
259
+ section: collected,
260
+ // 与 src/index.ts 对 JSON 分区现算体积的口径一致(utf8 字节数)
261
+ sizeBytes: new TextEncoder().encode(stringifyJsonSafe(data)).length,
262
+ items: unitsOf(data, tarballBytes),
263
+ };
264
+ }
265
+ /**
266
+ * 导出与预览的共同内核。
267
+ *
268
+ * `pack=false` 时跳过本地源打包(预览路径)—— 除该段外两条路径逐字共用,
269
+ * 避免「预览说会带什么」与「导出真的带什么」分叉。
270
+ */
271
+ async collect(ctx, options, pack) {
126
272
  const plugins = [];
127
273
  const warnings = [];
128
274
  try {
@@ -146,13 +292,13 @@ export class PluginsAdapter {
146
292
  warnings.push(msgOf(ctx)('adapter.pluginListReadFailed', { reason: err instanceof Error ? err.message : String(err) }));
147
293
  }
148
294
  const patch = [];
149
- try {
150
- const lines = await ctx.patchFile.readPatchLines(USER_PATCH_FILE);
151
- for (const l of lines)
152
- patch.push({ file: USER_PATCH_FILE, lineId: l.lineId, raw: l.raw });
153
- }
154
- catch (err) {
155
- warnings.push(msgOf(ctx)('adapter.patchReadFailed', { reason: err instanceof Error ? err.message : String(err) }));
295
+ // issue #71:patch 有两层(home 层 + profile 层),只读 home 层会漏掉 DSH 工具与 marketplace
296
+ // 写入 profile 层的激活行 —— 导出少了它们,导入到新机就少一批插件加载项。
297
+ const patchRead = await readEffectivePatchLines(ctx.patchFile, ctx.profile);
298
+ for (const l of patchRead.lines)
299
+ patch.push({ file: l.file, lineId: l.lineId, raw: l.raw });
300
+ for (const f of patchRead.failures) {
301
+ warnings.push(msgOf(ctx)('adapter.patchReadFailed', { reason: `${f.file}: ${f.reason}` }));
156
302
  }
157
303
  // pnpm-workspace.yaml(allowBuilds / minimumReleaseAgeExclude 等):随插件分区迁移,
158
304
  // 否则目标 profile 的 pnpm 可能因构建白名单/冷静期拒绝安装插件(§34.17 同款语义)。
@@ -210,7 +356,7 @@ export class PluginsAdapter {
210
356
  // (曾导致插件被静默丢失)。钩子由宿主注入;未注入 / 无本地源 / 单个失败一律不中断导出。
211
357
  let localTarballs;
212
358
  let effectivePlugins = plugins;
213
- if (this.localPack !== undefined) {
359
+ if (pack && this.localPack !== undefined) {
214
360
  try {
215
361
  const packed = await this.localPack(plugins, ctx);
216
362
  warnings.push(...packed.warnings);
@@ -282,45 +428,8 @@ export class PluginsAdapter {
282
428
  * 由选择模型保证勾选/取消同步。
283
429
  */
284
430
  listUnits(section) {
285
- const data = section.data;
286
- const wsId = 'plugins:pnpm-workspace';
287
- const patchFileIds = (data.patchFiles ?? []).map((pf) => `plugins:patch:${normalizePath(pf.relativePath)}`);
288
- const group = [wsId, ...patchFileIds];
289
- const units = [];
290
- for (const pl of data.plugins) {
291
- const tarball = (data.localTarballs ?? []).find((t) => t.packageName === pl.name);
292
- units.push({
293
- id: `plugin:${pl.name}`,
294
- label: pl.name,
295
- detail: pl.version,
296
- // base64 长度 → 原始字节数(仅用于展示体积,不求精确)
297
- sizeBytes: tarball !== undefined ? Math.floor((tarball.base64.length * 3) / 4) : 0,
298
- ...(tarball !== undefined ? { fileCount: 1 } : {}),
299
- });
300
- }
301
- for (const line of data.patch) {
302
- // 由 mcp / prompts adapter 管理的行不属本分区(applyItem 同样跳过),列出来只会误导用户
303
- if (isManagedElsewhere(line.raw))
304
- continue;
305
- units.push({ id: `patch:${line.lineId}`, label: line.lineId, sizeBytes: 0 });
306
- }
307
- if (data.pnpmWorkspace !== undefined && data.pnpmWorkspace !== null && data.pnpmWorkspace !== '') {
308
- units.push({
309
- id: wsId,
310
- label: 'pnpm-workspace.yaml',
311
- sizeBytes: data.pnpmWorkspace.length,
312
- ...(group.length > 1 ? { lockedWith: group } : {}),
313
- });
314
- }
315
- for (const pf of data.patchFiles ?? []) {
316
- units.push({
317
- id: `plugins:patch:${normalizePath(pf.relativePath)}`,
318
- label: pf.relativePath,
319
- sizeBytes: Math.floor((pf.base64.length * 3) / 4),
320
- ...(group.length > 1 ? { lockedWith: group } : {}),
321
- });
322
- }
323
- return units;
431
+ // 预览产物没有 tarball 字节,体积来自 measureLocalSources 挂上的统计(导出产物走 base64 分支)
432
+ return unitsOf(section.data, previewTarballBytes.get(section.data));
324
433
  }
325
434
  /**
326
435
  * 导入后目标机**可能**存在的 patch 文件集合(相对 profile 目录)——issue #35。
@@ -343,7 +452,13 @@ export class PluginsAdapter {
343
452
  if (await ctx.target.fs.exists(`${profileDir}/${rel}`))
344
453
  set.add(rel);
345
454
  }
346
- catch { /* 读不到 = 视为不存在 */ }
455
+ catch (err) {
456
+ // t81 / t23 口径:**读不到 ≠ 没有**。只有 ENOENT 才是「本来就没有」;EACCES/EBUSY/IO 一律上抛,
457
+ // 否则会把「声明在、文件也在」的 patch 当作不存在 → 从将要写入的 pnpm-workspace.yaml 里剔除
458
+ // (issue #35 的症状:目标机 pnpm 从此拒绝一切 add)。存在性探测自身失败不得降级成「不存在」。
459
+ if (!isENOENT(err))
460
+ throw err;
461
+ }
347
462
  }
348
463
  return set;
349
464
  }
@@ -466,17 +581,21 @@ export class PluginsAdapter {
466
581
  }
467
582
  // 用户 patch 行:lineId 唯一键;存在且同 → Skip;存在不同 → Conflict;不存在 → Create。
468
583
  // mcp-client 行与 systemPrompt/planMode 行由 mcp/prompts adapter 管理,此处跳过(避免重复写入覆盖)。
469
- const targetLines = await ctx.target.patchFile.readPatchLines(USER_PATCH_FILE);
584
+ // issue #71:目标行同样跨两层(否则 profile 层已有的行会被误判成 Create → 导入后出现重复行)
585
+ const targetLines = (await readEffectivePatchLines(ctx.target.patchFile, ctx.target.profile)).lines;
470
586
  for (const pl of data.patch) {
471
587
  if (isManagedElsewhere(pl.raw))
472
588
  continue;
473
589
  const id = `patch:${pl.lineId}`;
474
590
  const tl = targetLines.find((l) => l.lineId === pl.lineId);
591
+ // 层身份随计划项走:applyItem 按 pl.file 写回,导入前快照按 target.file 记原行,
592
+ // 两者必须是同一层,否则回滚会把另一层改坏(issue #71)。
593
+ const layer = resolveWriteLayer(pl.file, ctx.target.profile);
475
594
  if (!tl) {
476
595
  items.push({
477
596
  id, kind: 'Create', adapter: 'plugins',
478
597
  description: msg('adapter.patchLineCreate', { lineId: pl.lineId }), severity: 'info',
479
- target: { adapter: 'plugins', ref: pl.lineId },
598
+ target: { adapter: 'plugins', ref: pl.lineId, file: layer },
480
599
  });
481
600
  }
482
601
  else if (isDeepStrictEqual(tl.raw, pl.raw)) {
@@ -486,7 +605,7 @@ export class PluginsAdapter {
486
605
  items.push({
487
606
  id, kind: 'Conflict', adapter: 'plugins',
488
607
  description: msg('adapter.patchLineDiff', { lineId: pl.lineId }), severity: 'warning',
489
- target: { adapter: 'plugins', ref: pl.lineId },
608
+ target: { adapter: 'plugins', ref: pl.lineId, file: layer },
490
609
  });
491
610
  }
492
611
  }
@@ -542,7 +661,11 @@ export class PluginsAdapter {
542
661
  if (await ctx.target.fs.exists(`${profileDir}/${rel}`))
543
662
  available.add(rel);
544
663
  }
545
- catch { /* 读不到 = 视为不存在 */ }
664
+ catch (err) {
665
+ // 同上(t81 / t23):探测失败必须上抛,绝不把它降级成「声明不可满足」而写瘦 pnpm-workspace.yaml。
666
+ if (!isENOENT(err))
667
+ throw err;
668
+ }
546
669
  }
547
670
  const clean = sanitizePnpmWorkspacePatches(text, (rel) => available.has(rel));
548
671
  try {
@@ -631,7 +754,9 @@ export class PluginsAdapter {
631
754
  const pl = data?.patch.find((p) => p.lineId === ref);
632
755
  if (!pl)
633
756
  return { ok: false, message: msg('adapter.patchMissing', { ref }) };
634
- await ctx.target.patchFile.applyPatchChanges(pl.file, [
757
+ // 层身份按**语义**映射到目标机:来源是别的 profile 层时不能照抄名字(目标机未必有那个
758
+ // profile,宿主门面会拒绝),一律落到目标机当前 profile 层(与计划项 target.file 同口径)。
759
+ await ctx.target.patchFile.applyPatchChanges(resolveWriteLayer(pl.file, ctx.target.profile), [
635
760
  { lineId: ref, raw: pl.raw, action: item.kind === 'Create' ? 'insert' : 'update' },
636
761
  ]);
637
762
  return { ok: true, needsRestart: true, message: msg('adapter.patchWritten', { ref }) };
@@ -641,9 +766,33 @@ export class PluginsAdapter {
641
766
  if (!Array.isArray(section.plugins)) {
642
767
  issues.push({ path: 'plugins', message: msg('adapter.validate.array', { subject: 'plugins' }), severity: 'error' });
643
768
  }
769
+ else {
770
+ // ui-F3:条目级形状必须与 analyzeImport 的假设对齐(它直接读 p.name / p.version)。
771
+ // 只校验「是数组」会放过 plugins:[null],analyzeImport 抛内部 TypeError → 整包被拦下。
772
+ for (const [i, raw] of section.plugins.entries()) {
773
+ const p = raw;
774
+ if (p === null || typeof p !== 'object') {
775
+ issues.push({ path: `plugins[${i}]`, message: msg('adapter.validate.recordObject', { subject: 'plugin' }), severity: 'error' });
776
+ continue;
777
+ }
778
+ const name = p.name;
779
+ if (typeof name !== 'string' || name === '') {
780
+ issues.push({ path: `plugins[${i}].name`, message: msg('adapter.validate.requiredField', { subject: 'plugins[].name' }), severity: 'error' });
781
+ }
782
+ }
783
+ }
644
784
  if (section.patch !== undefined && !Array.isArray(section.patch)) {
645
785
  issues.push({ path: 'patch', message: msg('adapter.validate.array', { subject: 'patch' }), severity: 'error' });
646
786
  }
787
+ else if (Array.isArray(section.patch)) {
788
+ // ui-F3:patch 条目在 analyzeImport 里被读 pl.raw / pl.lineId(isManagedElsewhere(pl.raw))。
789
+ for (const [i, raw] of section.patch.entries()) {
790
+ const pl = raw;
791
+ if (pl === null || typeof pl !== 'object') {
792
+ issues.push({ path: `patch[${i}]`, message: msg('adapter.validate.recordObject', { subject: 'patch line' }), severity: 'error' });
793
+ }
794
+ }
795
+ }
647
796
  if (section.pnpmWorkspace !== undefined && section.pnpmWorkspace !== null && typeof section.pnpmWorkspace !== 'string') {
648
797
  issues.push({ path: 'pnpmWorkspace', message: msg('adapter.validate.string', { subject: 'pnpmWorkspace' }), severity: 'error' });
649
798
  }
@@ -1,18 +1,28 @@
1
1
  import type { MsgFunc } from '../core/messages.ts';
2
2
  import type { PromptEntry } from '../schema/types.ts';
3
3
  import type { ApplyResult, ConfigAdapter, ExportOptions, ExportSection, HostContext, ImportContext, PlanItem, ValidationResult } from '../core/types.ts';
4
- /** 导出记录:PromptEntry 之外记录来源行名(导入需要重建行时使用) */
4
+ /** 导出记录:PromptEntry 之外记录来源行名与所在层(导入重建 / 写回定位用) */
5
5
  export interface PromptExportEntry extends PromptEntry {
6
6
  sourceLineName?: string;
7
+ /**
8
+ * 来源 patch 层(相对 homeDir 的 POSIX 路径;见 core/patch-layers.ts)。
9
+ * 缺省 = 用户层(旧备份包没有该字段 → 按旧行为写回 home 层)。
10
+ */
11
+ sourceFile?: string;
7
12
  }
8
13
  export interface PromptsExportSection {
9
14
  version: 1;
10
15
  prompts: PromptExportEntry[];
11
16
  }
12
- /** 从 patch 行 config 中提取 prompt 条目(systemPrompt.persona / planMode.sections[].text) */
17
+ /**
18
+ * 从 patch 行 config 中提取 prompt 条目(systemPrompt.persona / planMode.sections[].text)。
19
+ *
20
+ * 行对象可带 `file`(层身份):带上时逐条目记录 `sourceFile`,导入写回落回原层。
21
+ */
13
22
  export declare function extractPrompts(lines: {
14
23
  lineId: string;
15
24
  raw: unknown;
25
+ file?: string;
16
26
  }[]): PromptExportEntry[];
17
27
  /** 把 prompt 文本合并进目标行 config(systemPrompt.persona 或 planMode.sections) */
18
28
  export declare function mergePromptIntoLine(raw: unknown, prompt: PromptExportEntry): Record<string, unknown>;
@@ -8,12 +8,19 @@
8
8
  * - 目标无该行但存在同名 prompt → Conflict(更新其所在行);
9
9
  * - 均不存在 → Warning(不编造行名自动创建,提示手动配置)。
10
10
  * rules/commands 无独立存储(研究报告 §2.2),不实现。
11
+ *
12
+ * issue #71:数据源与 mcp 分区同因 —— patch 有 home 层与 profile 层两层,此前只读 home 层,
13
+ * 于是写进 profile 层的 systemPrompt / planMode 在备份里全部丢失。现在两层都读、写回原层。
11
14
  */
12
15
  import { msgOf, zhMsg } from '../core/messages.js';
13
- import { USER_PATCH_FILE } from './plugins.js';
16
+ import { locatePatchLineLayer, readEffectivePatchLines, resolveWriteLayer } from '../core/patch-layers.js';
14
17
  import { sectionMeta } from '../schema/section-registry.js';
15
18
  import { validateJsonSection } from './json-section.js';
16
- /** 从 patch 行 config 中提取 prompt 条目(systemPrompt.persona / planMode.sections[].text) */
19
+ /**
20
+ * 从 patch 行 config 中提取 prompt 条目(systemPrompt.persona / planMode.sections[].text)。
21
+ *
22
+ * 行对象可带 `file`(层身份):带上时逐条目记录 `sourceFile`,导入写回落回原层。
23
+ */
17
24
  export function extractPrompts(lines) {
18
25
  const prompts = [];
19
26
  for (const line of lines) {
@@ -25,12 +32,12 @@ export function extractPrompts(lines) {
25
32
  const sourceLineName = typeof entry.name === 'string' ? entry.name : undefined;
26
33
  const sp = c['systemPrompt'];
27
34
  if (typeof sp === 'string' && sp.trim() !== '') {
28
- prompts.push({ id: `prompt:${line.lineId}:persona`, name: `${line.lineId}:persona`, kind: 'systemPrompt', text: sp, sourceLineId: line.lineId, sourceLineName });
35
+ prompts.push({ id: `prompt:${line.lineId}:persona`, name: `${line.lineId}:persona`, kind: 'systemPrompt', text: sp, sourceLineId: line.lineId, sourceLineName, ...(line.file !== undefined ? { sourceFile: line.file } : {}) });
29
36
  }
30
37
  else if (sp !== null && typeof sp === 'object' && typeof sp['persona'] === 'string') {
31
38
  const persona = sp['persona'];
32
39
  if (persona.trim() !== '') {
33
- prompts.push({ id: `prompt:${line.lineId}:persona`, name: `${line.lineId}:persona`, kind: 'systemPrompt', text: persona, sourceLineId: line.lineId, sourceLineName });
40
+ prompts.push({ id: `prompt:${line.lineId}:persona`, name: `${line.lineId}:persona`, kind: 'systemPrompt', text: persona, sourceLineId: line.lineId, sourceLineName, ...(line.file !== undefined ? { sourceFile: line.file } : {}) });
34
41
  }
35
42
  }
36
43
  const pm = c['planMode'];
@@ -40,7 +47,7 @@ export function extractPrompts(lines) {
40
47
  if (s === null || typeof s !== 'object' || typeof s['text'] !== 'string' || s['text'].trim() === '')
41
48
  continue;
42
49
  const name = typeof s['name'] === 'string' && s['name'] !== '' ? s['name'] : `${line.lineId}:section${i}`;
43
- prompts.push({ id: `prompt:${line.lineId}:${name}`, name, kind: 'planMode', text: s['text'], sourceLineId: line.lineId, sourceLineName });
50
+ prompts.push({ id: `prompt:${line.lineId}:${name}`, name, kind: 'planMode', text: s['text'], sourceLineId: line.lineId, sourceLineName, ...(line.file !== undefined ? { sourceFile: line.file } : {}) });
44
51
  }
45
52
  }
46
53
  }
@@ -108,14 +115,12 @@ export class PromptsAdapter {
108
115
  portability = sectionMeta('prompts').portability;
109
116
  async export(ctx, _options) {
110
117
  const warnings = [];
111
- let lines = [];
112
- try {
113
- lines = await ctx.patchFile.readPatchLines(USER_PATCH_FILE);
114
- }
115
- catch (err) {
116
- warnings.push(msgOf(ctx)('adapter.patchReadFailedPrompts', { reason: err instanceof Error ? err.message : String(err) }));
118
+ // issue #71:两层都读(home 层优先),否则 profile 层里的 persona / planMode 全部丢失
119
+ const read = await readEffectivePatchLines(ctx.patchFile, ctx.profile);
120
+ for (const f of read.failures) {
121
+ warnings.push(msgOf(ctx)('adapter.patchReadFailedPrompts', { reason: `${f.file}: ${f.reason}` }));
117
122
  }
118
- const prompts = extractPrompts(lines);
123
+ const prompts = extractPrompts(read.lines);
119
124
  return {
120
125
  sectionId: 'prompts',
121
126
  data: { version: 1, prompts },
@@ -126,7 +131,8 @@ export class PromptsAdapter {
126
131
  async analyzeImport(data, ctx) {
127
132
  const msg = ctx.msg;
128
133
  const items = [];
129
- const targetLines = await ctx.target.patchFile.readPatchLines(USER_PATCH_FILE);
134
+ // issue #71:目标行跨两层;同一 lineId 只保留优先级最高的一层(与 DSH 合并序一致)
135
+ const targetLines = (await readEffectivePatchLines(ctx.target.patchFile, ctx.target.profile)).lines;
130
136
  const targetPrompts = extractPrompts(targetLines);
131
137
  for (const p of data.prompts) {
132
138
  const id = p.id;
@@ -142,7 +148,8 @@ export class PromptsAdapter {
142
148
  description: msg('adapter.promptDiff', { name: p.name }),
143
149
  detail: `行 ${p.sourceLineId} current=${JSON.stringify(sameName.text).slice(0, 80)} imported=${JSON.stringify(p.text).slice(0, 80)}`,
144
150
  severity: 'warning',
145
- target: { adapter: 'prompts', ref: p.sourceLineId ?? '' },
151
+ // 层身份随计划项走:快照按这一层记原行、回滚才能写回原文件(issue #71)
152
+ target: { adapter: 'prompts', ref: p.sourceLineId ?? '', file: targetLine.file },
146
153
  });
147
154
  }
148
155
  }
@@ -156,7 +163,10 @@ export class PromptsAdapter {
156
163
  id, kind: 'Conflict', adapter: 'prompts',
157
164
  description: msg('adapter.promptDiffWithLine', { name: p.name, line: sameName.sourceLineId ?? '' }),
158
165
  severity: 'warning',
159
- target: { adapter: 'prompts', ref: sameName.sourceLineId ?? '' },
166
+ target: {
167
+ adapter: 'prompts', ref: sameName.sourceLineId ?? '',
168
+ file: targetLines.find((l) => l.lineId === sameName.sourceLineId)?.file,
169
+ },
160
170
  });
161
171
  }
162
172
  }
@@ -166,7 +176,7 @@ export class PromptsAdapter {
166
176
  items.push({
167
177
  id, kind: 'Create', adapter: 'prompts',
168
178
  description: msg('adapter.promptCreate', { name: p.name, line: lineId }), severity: 'info',
169
- target: { adapter: 'prompts', ref: lineId },
179
+ target: { adapter: 'prompts', ref: lineId, file: resolveWriteLayer(p.sourceFile, ctx.target.profile) },
170
180
  });
171
181
  }
172
182
  else {
@@ -193,18 +203,23 @@ export class PromptsAdapter {
193
203
  // Create:目标无来源行 → 用记录的行名重建 patch 行(insert)
194
204
  if (item.kind === 'Create') {
195
205
  const raw = buildPromptLine(ref, prompt);
196
- await ctx.target.patchFile.applyPatchChanges(USER_PATCH_FILE, [
206
+ // issue #71:备份里记了来源层就落「同语义层」(来源 profile 层 → 目标机当前 profile 层,
207
+ // 跨机器照抄 profile 名会被宿主门面拒绝);旧备份包无该字段 → 落 home 层(与改造前一致)
208
+ await ctx.target.patchFile.applyPatchChanges(resolveWriteLayer(prompt.sourceFile, ctx.target.profile), [
197
209
  { lineId: ref, raw, action: 'insert' },
198
210
  ]);
199
211
  return { ok: true, needsRestart: true, message: msg('adapter.promptCreated', { name: prompt.name, ref }) };
200
212
  }
201
213
  // Update / Conflict(useImported):合并进目标行 config
202
- const lines = await ctx.target.patchFile.readPatchLines(USER_PATCH_FILE);
214
+ // issue #71:先定位原行所在层(两层都没有 → home 层),再读该层的原行合并 —— 绝不把
215
+ // profile 层的行搬到 home 层(同一 lineId 两层各一份会让 DSH 的合并语义变得不可预期)
216
+ const file = await locatePatchLineLayer(ctx.target.patchFile, ctx.target.profile, ref);
217
+ const lines = await ctx.target.patchFile.readPatchLines(file);
203
218
  const line = lines.find((l) => l.lineId === ref);
204
219
  if (!line)
205
220
  return { ok: false, message: msg('adapter.patchLineMissing', { ref }) };
206
221
  const newRaw = mergePromptIntoLine(line.raw, prompt);
207
- await ctx.target.patchFile.applyPatchChanges(USER_PATCH_FILE, [
222
+ await ctx.target.patchFile.applyPatchChanges(file, [
208
223
  { lineId: ref, raw: newRaw, action: 'update' },
209
224
  ]);
210
225
  return { ok: true, needsRestart: true, message: msg('adapter.promptWritten', { name: prompt.name, ref }) };