dsh-config-manager 0.1.69 → 0.1.71

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 +5522 -1564
  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 +108 -0
  77. package/lib/foreign/claude-sessions.js +299 -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 +427 -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 +227 -0
  451. package/src/foreign/claude-sessions.test.ts +180 -0
  452. package/src/foreign/claude-sessions.ts +461 -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 +336 -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 +189 -0
  593. package/src/foreign/session-ir.ts +614 -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 +443 -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
@@ -10,25 +10,33 @@
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.ts';
15
16
  import { installSpecFor, resolveProcessProfileName } from '../core/plugin-cli.ts';
16
- import { LOCAL_PLUGIN_DIR } from '../core/local-plugin-pack.ts';
17
+ import { classifyPluginSpec, LOCAL_PLUGIN_DIR, resolveLocalPluginPath } from '../core/local-plugin-pack.ts';
17
18
  import type { PackLocalPluginsResult } from '../core/local-plugin-pack.ts';
18
19
  import { msgOf, zhMsg } from '../core/messages.ts';
19
20
  import type { MsgFunc } from '../core/messages.ts';
21
+ import { stringifyJsonSafe } from '../utils/json.ts';
20
22
  import { isPathSafe, normalizePath } from '../utils/paths.ts';
23
+ import { isENOENT } from '../utils/guards.ts';
21
24
  import { PLUGIN_PATCH_REF_PREFIX } from '../core/backup.ts';
25
+ import { readEffectivePatchLines, resolveWriteLayer, USER_PATCH_FILE } from '../core/patch-layers.ts';
22
26
  import { parsePnpmPatchedDependencies, sanitizePnpmWorkspacePatches } from './pnpm-workspace.ts';
23
27
  import type { LocalPluginTarball, PatchLine, PluginEntry, PluginsSection, PnpmPatchFile } from '../schema/types.ts';
24
28
  import type {
25
29
  ApplyResult, ConfigAdapter, ExportOptions, ExportSection, ExportUnit, HostContext,
26
- ImportContext, PlanItem, ValidationResult,
30
+ ImportContext, PlanItem, SectionPreview, ValidationResult,
27
31
  } from '../core/types.ts';
28
32
  import { sectionMeta } from '../schema/section-registry.ts';
29
33
  import { validateJsonSection } from './json-section.ts';
30
34
 
31
- export const USER_PATCH_FILE = 'cordis.patch.yml';
35
+ /**
36
+ * 用户(home)patch 层路径 —— 定义已移到 `core/patch-layers.ts`(issue #71:patch 有 home /
37
+ * profile 两层,层身份属于引擎知识,不属于某个 adapter)。此处 re-export 保持既有 import 面。
38
+ */
39
+ export { USER_PATCH_FILE };
32
40
 
33
41
  /**
34
42
  * 本地源插件打包钩子(由宿主注入,见 src/index.ts createAdapters)。
@@ -137,6 +145,109 @@ function entriesOfRaw(raw: unknown): { config?: unknown }[] {
137
145
  return [];
138
146
  }
139
147
 
148
+ /**
149
+ * 预览路径测到的本地源插件体积(`preview()` 写入,`listUnits()` 读取)。
150
+ *
151
+ * 为什么用 WeakMap 而不是给 `PluginsSection` 加字段:`localTarballs` 是备份格式的一部分
152
+ * (schema 校验、市场安全防线都在看它),而预览产物里本就没有 tarball 字节;把「只读预览测到的
153
+ * 体积」混进载荷只会让「预览说的」与「导出做的」更难分辨。挂在载荷对象上则随对象一起回收,
154
+ * 导出产物(真实 base64)走另一条分支,行为逐字不变。
155
+ */
156
+ const previewTarballBytes = new WeakMap<object, Map<string, number>>();
157
+
158
+ /**
159
+ * 零 spawn 推导本地源插件的体积(预览专用)。
160
+ *
161
+ * 两条路径,都是**只读度量**、都不 spawn 打包进程:
162
+ * - `file:` 源 → 一次 stat:源 tarball 本身就是 gzip 容器,字节数与 `npm pack` 产物同量级;
163
+ * - `link:` 源 → 目录体积上界(`fs.dirSizeBytes`):这是 `npm pack` 的输入目录,体积比
164
+ * pack 产物大(真机约 3.4 倍,pack 产物是 gzip),用来展示「量级」足够。
165
+ * 绝不在这里手写递归(见 local-plugin-pack.ts 安全约束:node_modules 由 npm pack 自己排除,
166
+ * 手写递归会把它整个算进来)——度量交给宿主门面,由它负责跳过 node_modules 与链接目录。
167
+ *
168
+ * 登记本身就是语义:Map 里有该插件 = 「该单元随备份携带一个文件」,与导出产物里
169
+ * `npm pack` 打出的 tarball(`fileCount: 1`)对齐;宿主未实现相应度量、越界路径
170
+ * (fs 门面对 home 外绝对路径抛 `host.fsPathEscape`)、目录读不到一律按 0 ——
171
+ * 预览绝不能因为一个体积数字而失败。
172
+ */
173
+ async function measureLocalSources(plugins: PluginEntry[], ctx: HostContext): Promise<Map<string, number>> {
174
+ const sizes = new Map<string, number>();
175
+ const statSize = ctx.fs.statSize;
176
+ const dirSize = ctx.fs.dirSizeBytes;
177
+ // profile 目录与打包钩子同源(local-plugin-host.ts:优先 profileDir,回退按 profile 名拼接)
178
+ const profileDir = ctx.profileDir !== undefined && ctx.profileDir !== ''
179
+ ? ctx.profileDir
180
+ : join(ctx.homeDir, PNPM_PROFILE_DIR(ctx.profile));
181
+ for (const pl of plugins) {
182
+ const kind = classifyPluginSpec(pl.spec);
183
+ if (kind !== 'file' && kind !== 'link') continue;
184
+ try {
185
+ const abs = resolveLocalPluginPath(pl.spec ?? '', { homeDir: ctx.homeDir, profileDir });
186
+ // 宿主未实现对应度量 → null → 按 0(旧行为,只少一个数字)
187
+ const size = kind === 'link'
188
+ ? (dirSize !== undefined ? await dirSize.call(ctx.fs, abs) : null)
189
+ : (statSize !== undefined ? await statSize.call(ctx.fs, abs) : null);
190
+ sizes.set(pl.name, size !== null && size > 0 ? size : 0);
191
+ } catch {
192
+ // 越界 / 读不到:按 0 处理(预览不因此失败),但仍算「随包携带一个文件」
193
+ sizes.set(pl.name, 0);
194
+ }
195
+ }
196
+ return sizes;
197
+ }
198
+
199
+ /**
200
+ * 由分区载荷派生单元清单(纯函数、零 I/O)。
201
+ *
202
+ * 单元 = 插件包 / cordis 补丁行 / pnpm-workspace.yaml / 单个 patch 文件。
203
+ * 后两者构成**原子组**:patchedDependencies 的声明与 patches/** 文件必须同进同出
204
+ * (issue #35 —— 只搬声明会让目标机 pnpm 拒绝一切 add),故互相写进 lockedWith,
205
+ * 由选择模型保证勾选/取消同步。
206
+ *
207
+ * `tarballBytes` = 预览路径登记到的本地源体积(见 measureLocalSources,0 = 体积未知但仍随包携带);
208
+ * 缺省时体积与文件数完全按导出产物里的 base64 计算 —— 与改造前逐字一致。
209
+ */
210
+ function unitsOf(data: PluginsSection, tarballBytes?: Map<string, number>): ExportUnit[] {
211
+ const wsId = 'plugins:pnpm-workspace';
212
+ const patchFileIds = (data.patchFiles ?? []).map((pf) => `plugins:patch:${normalizePath(pf.relativePath)}`);
213
+ const group = [wsId, ...patchFileIds];
214
+ const units: ExportUnit[] = [];
215
+ for (const pl of data.plugins) {
216
+ const tarball = tarballOf(data, pl.name);
217
+ const measured = tarballBytes?.get(pl.name);
218
+ units.push({
219
+ id: `plugin:${pl.name}`,
220
+ label: pl.name,
221
+ detail: pl.version,
222
+ // base64 长度 → 原始字节数(仅用于展示体积,不求精确);预览路径没有字节,用 stat 量到的源体积
223
+ sizeBytes: tarball !== undefined ? Math.floor((tarball.base64.length * 3) / 4) : measured ?? 0,
224
+ ...(tarball !== undefined || measured !== undefined ? { fileCount: 1 } : {}),
225
+ });
226
+ }
227
+ for (const line of data.patch) {
228
+ // 由 mcp / prompts adapter 管理的行不属本分区(applyItem 同样跳过),列出来只会误导用户
229
+ if (isManagedElsewhere(line.raw)) continue;
230
+ units.push({ id: `patch:${line.lineId}`, label: line.lineId, sizeBytes: 0 });
231
+ }
232
+ if (data.pnpmWorkspace !== undefined && data.pnpmWorkspace !== null && data.pnpmWorkspace !== '') {
233
+ units.push({
234
+ id: wsId,
235
+ label: 'pnpm-workspace.yaml',
236
+ sizeBytes: data.pnpmWorkspace.length,
237
+ ...(group.length > 1 ? { lockedWith: group } : {}),
238
+ });
239
+ }
240
+ for (const pf of data.patchFiles ?? []) {
241
+ units.push({
242
+ id: `plugins:patch:${normalizePath(pf.relativePath)}`,
243
+ label: pf.relativePath,
244
+ sizeBytes: Math.floor((pf.base64.length * 3) / 4),
245
+ ...(group.length > 1 ? { lockedWith: group } : {}),
246
+ });
247
+ }
248
+ return units;
249
+ }
250
+
140
251
  export class PluginsAdapter implements ConfigAdapter<PluginsSection> {
141
252
  readonly id = 'plugins' as const;
142
253
  // 元数据唯一来源 = 注册表(t31):不再与 ui/export-flow.ts 的导出目录各写一份
@@ -154,6 +265,42 @@ export class PluginsAdapter implements ConfigAdapter<PluginsSection> {
154
265
  }
155
266
 
156
267
  async export(ctx: HostContext, options: ExportOptions): Promise<ExportSection<PluginsSection>> {
268
+ return this.collect(ctx, options, true);
269
+ }
270
+
271
+ /**
272
+ * 只读预览(`/export-preview` 用):与 `export()` **同口径**,但不做任何本地源打包。
273
+ *
274
+ * 为什么必须覆写:基类缺省会让调用方退回 `export()`,而 `export()` 会为每个 `link:`/`file:`
275
+ * 插件 spawn 一次 `npm pack`(真机 12 个本地源插件 ≈12 s)。「总览」页只想知道条目数与体积,
276
+ * 却因此每次都要打包一遍全部本地插件 —— 预览是纯只读路径,不该有任何进程副作用。
277
+ *
278
+ * 与 `export()` 的**唯一**差异:
279
+ * - 不注入 `localTarballs`(没有字节),故清单 spec 保持原样(不做 `file:<tarball>` 改写);
280
+ * - 插件单元体积改为 `file:` 源的一次 stat(`measureLocalSources`),`link:` 目录按 0;
281
+ * - 体积用一次 `stringify` 算 UTF-8 字节数(与 /export-preview 对 JSON 分区的口径一致)。
282
+ * 其余(清单过滤、patch 行、pnpm-workspace、patchFiles、条目级白名单、告警文案、单元分组)逐字相同。
283
+ */
284
+ async preview(ctx: HostContext, options: ExportOptions): Promise<SectionPreview<PluginsSection>> {
285
+ const collected = await this.collect(ctx, options, false);
286
+ const data = collected.data;
287
+ const tarballBytes = await measureLocalSources(data.plugins, ctx);
288
+ if (tarballBytes.size > 0) previewTarballBytes.set(data, tarballBytes);
289
+ return {
290
+ section: collected,
291
+ // 与 src/index.ts 对 JSON 分区现算体积的口径一致(utf8 字节数)
292
+ sizeBytes: new TextEncoder().encode(stringifyJsonSafe(data)).length,
293
+ items: unitsOf(data, tarballBytes),
294
+ };
295
+ }
296
+
297
+ /**
298
+ * 导出与预览的共同内核。
299
+ *
300
+ * `pack=false` 时跳过本地源打包(预览路径)—— 除该段外两条路径逐字共用,
301
+ * 避免「预览说会带什么」与「导出真的带什么」分叉。
302
+ */
303
+ private async collect(ctx: HostContext, options: ExportOptions, pack: boolean): Promise<ExportSection<PluginsSection>> {
157
304
  const plugins: PluginEntry[] = [];
158
305
  const warnings: string[] = [];
159
306
  try {
@@ -175,11 +322,12 @@ export class PluginsAdapter implements ConfigAdapter<PluginsSection> {
175
322
  warnings.push(msgOf(ctx)('adapter.pluginListReadFailed', { reason: err instanceof Error ? err.message : String(err) }));
176
323
  }
177
324
  const patch: PatchLine[] = [];
178
- try {
179
- const lines = await ctx.patchFile.readPatchLines(USER_PATCH_FILE);
180
- for (const l of lines) patch.push({ file: USER_PATCH_FILE, lineId: l.lineId, raw: l.raw });
181
- } catch (err) {
182
- warnings.push(msgOf(ctx)('adapter.patchReadFailed', { reason: err instanceof Error ? err.message : String(err) }));
325
+ // issue #71:patch 有两层(home 层 + profile 层),只读 home 层会漏掉 DSH 工具与 marketplace
326
+ // 写入 profile 层的激活行 —— 导出少了它们,导入到新机就少一批插件加载项。
327
+ const patchRead = await readEffectivePatchLines(ctx.patchFile, ctx.profile);
328
+ for (const l of patchRead.lines) patch.push({ file: l.file, lineId: l.lineId, raw: l.raw });
329
+ for (const f of patchRead.failures) {
330
+ warnings.push(msgOf(ctx)('adapter.patchReadFailed', { reason: `${f.file}: ${f.reason}` }));
183
331
  }
184
332
  // pnpm-workspace.yaml(allowBuilds / minimumReleaseAgeExclude 等):随插件分区迁移,
185
333
  // 否则目标 profile 的 pnpm 可能因构建白名单/冷静期拒绝安装插件(§34.17 同款语义)。
@@ -236,7 +384,7 @@ export class PluginsAdapter implements ConfigAdapter<PluginsSection> {
236
384
  // (曾导致插件被静默丢失)。钩子由宿主注入;未注入 / 无本地源 / 单个失败一律不中断导出。
237
385
  let localTarballs: LocalPluginTarball[] | undefined;
238
386
  let effectivePlugins = plugins;
239
- if (this.localPack !== undefined) {
387
+ if (pack && this.localPack !== undefined) {
240
388
  try {
241
389
  const packed = await this.localPack(plugins, ctx);
242
390
  warnings.push(...packed.warnings);
@@ -312,44 +460,8 @@ export class PluginsAdapter implements ConfigAdapter<PluginsSection> {
312
460
  * 由选择模型保证勾选/取消同步。
313
461
  */
314
462
  listUnits(section: ExportSection<PluginsSection>): ExportUnit[] {
315
- const data = section.data;
316
- const wsId = 'plugins:pnpm-workspace';
317
- const patchFileIds = (data.patchFiles ?? []).map((pf) => `plugins:patch:${normalizePath(pf.relativePath)}`);
318
- const group = [wsId, ...patchFileIds];
319
- const units: ExportUnit[] = [];
320
- for (const pl of data.plugins) {
321
- const tarball = (data.localTarballs ?? []).find((t) => t.packageName === pl.name);
322
- units.push({
323
- id: `plugin:${pl.name}`,
324
- label: pl.name,
325
- detail: pl.version,
326
- // base64 长度 → 原始字节数(仅用于展示体积,不求精确)
327
- sizeBytes: tarball !== undefined ? Math.floor((tarball.base64.length * 3) / 4) : 0,
328
- ...(tarball !== undefined ? { fileCount: 1 } : {}),
329
- });
330
- }
331
- for (const line of data.patch) {
332
- // 由 mcp / prompts adapter 管理的行不属本分区(applyItem 同样跳过),列出来只会误导用户
333
- if (isManagedElsewhere(line.raw)) continue;
334
- units.push({ id: `patch:${line.lineId}`, label: line.lineId, sizeBytes: 0 });
335
- }
336
- if (data.pnpmWorkspace !== undefined && data.pnpmWorkspace !== null && data.pnpmWorkspace !== '') {
337
- units.push({
338
- id: wsId,
339
- label: 'pnpm-workspace.yaml',
340
- sizeBytes: data.pnpmWorkspace.length,
341
- ...(group.length > 1 ? { lockedWith: group } : {}),
342
- });
343
- }
344
- for (const pf of data.patchFiles ?? []) {
345
- units.push({
346
- id: `plugins:patch:${normalizePath(pf.relativePath)}`,
347
- label: pf.relativePath,
348
- sizeBytes: Math.floor((pf.base64.length * 3) / 4),
349
- ...(group.length > 1 ? { lockedWith: group } : {}),
350
- });
351
- }
352
- return units;
463
+ // 预览产物没有 tarball 字节,体积来自 measureLocalSources 挂上的统计(导出产物走 base64 分支)
464
+ return unitsOf(section.data, previewTarballBytes.get(section.data));
353
465
  }
354
466
 
355
467
  /**
@@ -369,7 +481,12 @@ export class PluginsAdapter implements ConfigAdapter<PluginsSection> {
369
481
  if (rel === '' || !isPathSafe(rel) || set.has(rel)) continue;
370
482
  try {
371
483
  if (await ctx.target.fs.exists(`${profileDir}/${rel}`)) set.add(rel);
372
- } catch { /* 读不到 = 视为不存在 */ }
484
+ } catch (err) {
485
+ // t81 / t23 口径:**读不到 ≠ 没有**。只有 ENOENT 才是「本来就没有」;EACCES/EBUSY/IO 一律上抛,
486
+ // 否则会把「声明在、文件也在」的 patch 当作不存在 → 从将要写入的 pnpm-workspace.yaml 里剔除
487
+ // (issue #35 的症状:目标机 pnpm 从此拒绝一切 add)。存在性探测自身失败不得降级成「不存在」。
488
+ if (!isENOENT(err)) throw err;
489
+ }
373
490
  }
374
491
  return set;
375
492
  }
@@ -491,16 +608,20 @@ export class PluginsAdapter implements ConfigAdapter<PluginsSection> {
491
608
 
492
609
  // 用户 patch 行:lineId 唯一键;存在且同 → Skip;存在不同 → Conflict;不存在 → Create。
493
610
  // mcp-client 行与 systemPrompt/planMode 行由 mcp/prompts adapter 管理,此处跳过(避免重复写入覆盖)。
494
- const targetLines = await ctx.target.patchFile.readPatchLines(USER_PATCH_FILE);
611
+ // issue #71:目标行同样跨两层(否则 profile 层已有的行会被误判成 Create → 导入后出现重复行)
612
+ const targetLines = (await readEffectivePatchLines(ctx.target.patchFile, ctx.target.profile)).lines;
495
613
  for (const pl of data.patch) {
496
614
  if (isManagedElsewhere(pl.raw)) continue;
497
615
  const id = `patch:${pl.lineId}`;
498
616
  const tl = targetLines.find((l) => l.lineId === pl.lineId);
617
+ // 层身份随计划项走:applyItem 按 pl.file 写回,导入前快照按 target.file 记原行,
618
+ // 两者必须是同一层,否则回滚会把另一层改坏(issue #71)。
619
+ const layer = resolveWriteLayer(pl.file, ctx.target.profile);
499
620
  if (!tl) {
500
621
  items.push({
501
622
  id, kind: 'Create', adapter: 'plugins',
502
623
  description: msg('adapter.patchLineCreate', { lineId: pl.lineId }), severity: 'info',
503
- target: { adapter: 'plugins', ref: pl.lineId },
624
+ target: { adapter: 'plugins', ref: pl.lineId, file: layer },
504
625
  });
505
626
  } else if (isDeepStrictEqual(tl.raw, pl.raw)) {
506
627
  items.push({ id, kind: 'Skip', adapter: 'plugins', description: msg('adapter.patchLineSame', { lineId: pl.lineId }), severity: 'info' });
@@ -508,7 +629,7 @@ export class PluginsAdapter implements ConfigAdapter<PluginsSection> {
508
629
  items.push({
509
630
  id, kind: 'Conflict', adapter: 'plugins',
510
631
  description: msg('adapter.patchLineDiff', { lineId: pl.lineId }), severity: 'warning',
511
- target: { adapter: 'plugins', ref: pl.lineId },
632
+ target: { adapter: 'plugins', ref: pl.lineId, file: layer },
512
633
  });
513
634
  }
514
635
  }
@@ -561,7 +682,10 @@ export class PluginsAdapter implements ConfigAdapter<PluginsSection> {
561
682
  if (rel === '' || !isPathSafe(rel) || available.has(rel)) continue;
562
683
  try {
563
684
  if (await ctx.target.fs.exists(`${profileDir}/${rel}`)) available.add(rel);
564
- } catch { /* 读不到 = 视为不存在 */ }
685
+ } catch (err) {
686
+ // 同上(t81 / t23):探测失败必须上抛,绝不把它降级成「声明不可满足」而写瘦 pnpm-workspace.yaml。
687
+ if (!isENOENT(err)) throw err;
688
+ }
565
689
  }
566
690
  const clean = sanitizePnpmWorkspacePatches(text, (rel) => available.has(rel));
567
691
  try {
@@ -646,7 +770,9 @@ export class PluginsAdapter implements ConfigAdapter<PluginsSection> {
646
770
  const data = ctx.sections.get('plugins') as PluginsSection | undefined;
647
771
  const pl = data?.patch.find((p) => p.lineId === ref);
648
772
  if (!pl) return { ok: false, message: msg('adapter.patchMissing', { ref }) };
649
- await ctx.target.patchFile.applyPatchChanges(pl.file, [
773
+ // 层身份按**语义**映射到目标机:来源是别的 profile 层时不能照抄名字(目标机未必有那个
774
+ // profile,宿主门面会拒绝),一律落到目标机当前 profile 层(与计划项 target.file 同口径)。
775
+ await ctx.target.patchFile.applyPatchChanges(resolveWriteLayer(pl.file, ctx.target.profile), [
650
776
  { lineId: ref, raw: pl.raw, action: item.kind === 'Create' ? 'insert' : 'update' },
651
777
  ]);
652
778
  return { ok: true, needsRestart: true, message: msg('adapter.patchWritten', { ref }) };
@@ -656,9 +782,31 @@ export class PluginsAdapter implements ConfigAdapter<PluginsSection> {
656
782
  return validateJsonSection<PluginsSection>('plugins', data, msg, (section, issues) => {
657
783
  if (!Array.isArray(section.plugins)) {
658
784
  issues.push({ path: 'plugins', message: msg('adapter.validate.array', { subject: 'plugins' }), severity: 'error' });
785
+ } else {
786
+ // ui-F3:条目级形状必须与 analyzeImport 的假设对齐(它直接读 p.name / p.version)。
787
+ // 只校验「是数组」会放过 plugins:[null],analyzeImport 抛内部 TypeError → 整包被拦下。
788
+ for (const [i, raw] of section.plugins.entries()) {
789
+ const p = raw as unknown;
790
+ if (p === null || typeof p !== 'object') {
791
+ issues.push({ path: `plugins[${i}]`, message: msg('adapter.validate.recordObject', { subject: 'plugin' }), severity: 'error' });
792
+ continue;
793
+ }
794
+ const name = (p as { name?: unknown }).name;
795
+ if (typeof name !== 'string' || name === '') {
796
+ issues.push({ path: `plugins[${i}].name`, message: msg('adapter.validate.requiredField', { subject: 'plugins[].name' }), severity: 'error' });
797
+ }
798
+ }
659
799
  }
660
800
  if (section.patch !== undefined && !Array.isArray(section.patch)) {
661
801
  issues.push({ path: 'patch', message: msg('adapter.validate.array', { subject: 'patch' }), severity: 'error' });
802
+ } else if (Array.isArray(section.patch)) {
803
+ // ui-F3:patch 条目在 analyzeImport 里被读 pl.raw / pl.lineId(isManagedElsewhere(pl.raw))。
804
+ for (const [i, raw] of section.patch.entries()) {
805
+ const pl = raw as unknown;
806
+ if (pl === null || typeof pl !== 'object') {
807
+ issues.push({ path: `patch[${i}]`, message: msg('adapter.validate.recordObject', { subject: 'patch line' }), severity: 'error' });
808
+ }
809
+ }
662
810
  }
663
811
  if (section.pnpmWorkspace !== undefined && section.pnpmWorkspace !== null && typeof section.pnpmWorkspace !== 'string') {
664
812
  issues.push({ path: 'pnpmWorkspace', message: msg('adapter.validate.string', { subject: 'pnpmWorkspace' }), severity: 'error' });
@@ -101,6 +101,55 @@ test('prompts: 同行同内容 Skip / 不同 Conflict(update) / 目标无 → Wa
101
101
  assert.equal(w.ok, true, 'Warning 项 applyItem 无副作用');
102
102
  });
103
103
 
104
+ test('issue #71: profile 层 prompt 行也要导出(含 sourceFile),Create 落目标机当前 profile 层', async () => {
105
+ const PROFILE_REL = 'profiles/tauri/cordis.patch.yml';
106
+ const src = makeContext('win32', 'C:\\Users\\alice', 'tauri');
107
+ src.patchFile.bucket(PROFILE_REL).set('persona-line', LINES[0]!);
108
+ const adapter = new PromptsAdapter();
109
+ const exported = await adapter.export(src, { includeSecrets: false });
110
+ assert.equal(exported.data.prompts.length, 1, 'profile 层的 persona 必须在备份里(此前只读 home 层 → 恒为空)');
111
+ assert.equal(exported.data.prompts[0]?.sourceFile, PROFILE_REL, '必须记住来源层');
112
+ const sections = new Map([['prompts', exported.data]]);
113
+
114
+ // 目标机 profile 名不同(tauri → web):Create 必须落**目标机自己的** profile 层
115
+ const dst = makeContext('linux', '/home/bob', 'web');
116
+ const items = await adapter.analyzeImport(exported.data, makeImportContext(dst, sections));
117
+ assert.equal(items[0]?.kind, 'Create');
118
+ const r = await adapter.applyItem(items[0]!, makeImportContext(dst, sections));
119
+ assert.equal(r.ok, true);
120
+ assert.equal(dst.patchFile.bucket('profiles/web/cordis.patch.yml').has('persona-line'), true, '落目标机当前 profile 层');
121
+ assert.equal(dst.patchFile.lines.size, 0, '不得搬到 home 层');
122
+
123
+ // Update:目标机该行在 profile 层 → 合并回 profile 层(不搬家、不新建)
124
+ const dst2 = makeContext('linux', '/home/bob', 'web');
125
+ dst2.patchFile.bucket('profiles/web/cordis.patch.yml').set('persona-line', {
126
+ lineId: 'persona-line',
127
+ raw: { id: 'persona-line', name: '@deepseek-ai/dsh-web', config: { systemPrompt: { persona: 'Old persona.' } } },
128
+ });
129
+ const items2 = await adapter.analyzeImport(exported.data, makeImportContext(dst2, sections));
130
+ assert.equal(items2[0]?.kind, 'Conflict');
131
+ const r2 = await adapter.applyItem({ ...items2[0]!, kind: 'Update' }, makeImportContext(dst2, sections));
132
+ assert.equal(r2.ok, true);
133
+ const merged = dst2.patchFile.bucket('profiles/web/cordis.patch.yml').get('persona-line')?.raw as Record<string, unknown>;
134
+ const sp = (merged['config'] as Record<string, unknown>)['systemPrompt'] as Record<string, unknown>;
135
+ assert.equal(sp['persona'], 'You are a helpful coding assistant.');
136
+ assert.equal(dst2.patchFile.lines.size, 0, 'Update 同样不得在 home 层留下副本');
137
+ });
138
+
139
+ test('issue #71: 同名行两层都有 → 只导出优先级更高的一层(home 覆盖 profile)', async () => {
140
+ const src = makeContext('win32', 'C:\\Users\\alice', 'tauri');
141
+ const profileRel = 'profiles/tauri/cordis.patch.yml';
142
+ src.patchFile.lines.set('plan-line', LINES[1]!);
143
+ src.patchFile.bucket(profileRel).set('plan-line', {
144
+ lineId: 'plan-line',
145
+ raw: { id: 'plan-line', name: 'plan-mode', config: { planMode: { sections: [{ name: 'plan', order: 1, text: 'Profile version.' }] } } },
146
+ });
147
+ const exported = await new PromptsAdapter().export(src, { includeSecrets: false });
148
+ assert.equal(exported.data.prompts.length, 1);
149
+ assert.equal(exported.data.prompts[0]?.text, 'Plan first, then code.', '生效值是 home 层');
150
+ assert.equal(exported.data.prompts[0]?.sourceFile, 'cordis.patch.yml');
151
+ });
152
+
104
153
  test('prompts: validate', async () => {
105
154
  const adapter = new PromptsAdapter();
106
155
  const ok = await adapter.validate({ version: 1, prompts: [{ id: 'a', name: 'n', kind: 'systemPrompt', text: 't' }] });
@@ -8,6 +8,9 @@
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.ts';
13
16
  import type { MsgFunc } from '../core/messages.ts';
@@ -16,19 +19,28 @@ import type {
16
19
  ApplyResult, ConfigAdapter, ExportOptions, ExportSection, HostContext,
17
20
  ImportContext, PlanItem, ValidationResult,
18
21
  } from '../core/types.ts';
19
- import { USER_PATCH_FILE } from './plugins.ts';
22
+ import { locatePatchLineLayer, readEffectivePatchLines, resolveWriteLayer } from '../core/patch-layers.ts';
20
23
  import { sectionMeta } from '../schema/section-registry.ts';
21
24
  import { validateJsonSection } from './json-section.ts';
22
25
 
23
- /** 导出记录:PromptEntry 之外记录来源行名(导入需要重建行时使用) */
26
+ /** 导出记录:PromptEntry 之外记录来源行名与所在层(导入重建 / 写回定位用) */
24
27
  export interface PromptExportEntry extends PromptEntry {
25
28
  sourceLineName?: string;
29
+ /**
30
+ * 来源 patch 层(相对 homeDir 的 POSIX 路径;见 core/patch-layers.ts)。
31
+ * 缺省 = 用户层(旧备份包没有该字段 → 按旧行为写回 home 层)。
32
+ */
33
+ sourceFile?: string;
26
34
  }
27
35
 
28
36
  export interface PromptsExportSection { version: 1; prompts: PromptExportEntry[]; }
29
37
 
30
- /** 从 patch 行 config 中提取 prompt 条目(systemPrompt.persona / planMode.sections[].text) */
31
- export function extractPrompts(lines: { lineId: string; raw: unknown }[]): PromptExportEntry[] {
38
+ /**
39
+ * 从 patch 行 config 中提取 prompt 条目(systemPrompt.persona / planMode.sections[].text)。
40
+ *
41
+ * 行对象可带 `file`(层身份):带上时逐条目记录 `sourceFile`,导入写回落回原层。
42
+ */
43
+ export function extractPrompts(lines: { lineId: string; raw: unknown; file?: string }[]): PromptExportEntry[] {
32
44
  const prompts: PromptExportEntry[] = [];
33
45
  for (const line of lines) {
34
46
  for (const entry of entriesOf(line.raw)) {
@@ -39,11 +51,11 @@ export function extractPrompts(lines: { lineId: string; raw: unknown }[]): Promp
39
51
 
40
52
  const sp = c['systemPrompt'];
41
53
  if (typeof sp === 'string' && sp.trim() !== '') {
42
- prompts.push({ id: `prompt:${line.lineId}:persona`, name: `${line.lineId}:persona`, kind: 'systemPrompt', text: sp, sourceLineId: line.lineId, sourceLineName });
54
+ 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 } : {}) });
43
55
  } else if (sp !== null && typeof sp === 'object' && typeof (sp as Record<string, unknown>)['persona'] === 'string') {
44
56
  const persona = (sp as Record<string, unknown>)['persona'] as string;
45
57
  if (persona.trim() !== '') {
46
- prompts.push({ id: `prompt:${line.lineId}:persona`, name: `${line.lineId}:persona`, kind: 'systemPrompt', text: persona, sourceLineId: line.lineId, sourceLineName });
58
+ 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 } : {}) });
47
59
  }
48
60
  }
49
61
 
@@ -53,7 +65,7 @@ export function extractPrompts(lines: { lineId: string; raw: unknown }[]): Promp
53
65
  for (const [i, s] of sections.entries()) {
54
66
  if (s === null || typeof s !== 'object' || typeof s['text'] !== 'string' || (s['text'] as string).trim() === '') continue;
55
67
  const name = typeof s['name'] === 'string' && s['name'] !== '' ? s['name'] : `${line.lineId}:section${i}`;
56
- prompts.push({ id: `prompt:${line.lineId}:${name}`, name, kind: 'planMode', text: s['text'] as string, sourceLineId: line.lineId, sourceLineName });
68
+ prompts.push({ id: `prompt:${line.lineId}:${name}`, name, kind: 'planMode', text: s['text'] as string, sourceLineId: line.lineId, sourceLineName, ...(line.file !== undefined ? { sourceFile: line.file } : {}) });
57
69
  }
58
70
  }
59
71
  }
@@ -116,13 +128,12 @@ export class PromptsAdapter implements ConfigAdapter<PromptsExportSection> {
116
128
 
117
129
  async export(ctx: HostContext, _options: ExportOptions): Promise<ExportSection<PromptsExportSection>> {
118
130
  const warnings: string[] = [];
119
- let lines: { lineId: string; raw: unknown }[] = [];
120
- try {
121
- lines = await ctx.patchFile.readPatchLines(USER_PATCH_FILE);
122
- } catch (err) {
123
- warnings.push(msgOf(ctx)('adapter.patchReadFailedPrompts', { reason: err instanceof Error ? err.message : String(err) }));
131
+ // issue #71:两层都读(home 层优先),否则 profile 层里的 persona / planMode 全部丢失
132
+ const read = await readEffectivePatchLines(ctx.patchFile, ctx.profile);
133
+ for (const f of read.failures) {
134
+ warnings.push(msgOf(ctx)('adapter.patchReadFailedPrompts', { reason: `${f.file}: ${f.reason}` }));
124
135
  }
125
- const prompts = extractPrompts(lines);
136
+ const prompts = extractPrompts(read.lines);
126
137
  return {
127
138
  sectionId: 'prompts',
128
139
  data: { version: 1, prompts },
@@ -134,7 +145,8 @@ export class PromptsAdapter implements ConfigAdapter<PromptsExportSection> {
134
145
  async analyzeImport(data: PromptsExportSection, ctx: ImportContext): Promise<PlanItem[]> {
135
146
  const msg = ctx.msg;
136
147
  const items: PlanItem[] = [];
137
- const targetLines = await ctx.target.patchFile.readPatchLines(USER_PATCH_FILE);
148
+ // issue #71:目标行跨两层;同一 lineId 只保留优先级最高的一层(与 DSH 合并序一致)
149
+ const targetLines = (await readEffectivePatchLines(ctx.target.patchFile, ctx.target.profile)).lines;
138
150
  const targetPrompts = extractPrompts(targetLines);
139
151
  for (const p of data.prompts) {
140
152
  const id = p.id;
@@ -149,7 +161,8 @@ export class PromptsAdapter implements ConfigAdapter<PromptsExportSection> {
149
161
  description: msg('adapter.promptDiff', { name: p.name }),
150
162
  detail: `行 ${p.sourceLineId} current=${JSON.stringify(sameName.text).slice(0, 80)} imported=${JSON.stringify(p.text).slice(0, 80)}`,
151
163
  severity: 'warning',
152
- target: { adapter: 'prompts', ref: p.sourceLineId ?? '' },
164
+ // 层身份随计划项走:快照按这一层记原行、回滚才能写回原文件(issue #71)
165
+ target: { adapter: 'prompts', ref: p.sourceLineId ?? '', file: targetLine.file },
153
166
  });
154
167
  }
155
168
  } else if (sameName) {
@@ -161,7 +174,10 @@ export class PromptsAdapter implements ConfigAdapter<PromptsExportSection> {
161
174
  id, kind: 'Conflict', adapter: 'prompts',
162
175
  description: msg('adapter.promptDiffWithLine', { name: p.name, line: sameName.sourceLineId ?? '' }),
163
176
  severity: 'warning',
164
- target: { adapter: 'prompts', ref: sameName.sourceLineId ?? '' },
177
+ target: {
178
+ adapter: 'prompts', ref: sameName.sourceLineId ?? '',
179
+ file: targetLines.find((l) => l.lineId === sameName.sourceLineId)?.file,
180
+ },
165
181
  });
166
182
  }
167
183
  } else if (p.sourceLineName) {
@@ -170,7 +186,7 @@ export class PromptsAdapter implements ConfigAdapter<PromptsExportSection> {
170
186
  items.push({
171
187
  id, kind: 'Create', adapter: 'prompts',
172
188
  description: msg('adapter.promptCreate', { name: p.name, line: lineId }), severity: 'info',
173
- target: { adapter: 'prompts', ref: lineId },
189
+ target: { adapter: 'prompts', ref: lineId, file: resolveWriteLayer(p.sourceFile, ctx.target.profile) },
174
190
  });
175
191
  } else {
176
192
  items.push({
@@ -194,17 +210,22 @@ export class PromptsAdapter implements ConfigAdapter<PromptsExportSection> {
194
210
  // Create:目标无来源行 → 用记录的行名重建 patch 行(insert)
195
211
  if (item.kind === 'Create') {
196
212
  const raw = buildPromptLine(ref, prompt);
197
- await ctx.target.patchFile.applyPatchChanges(USER_PATCH_FILE, [
213
+ // issue #71:备份里记了来源层就落「同语义层」(来源 profile 层 → 目标机当前 profile 层,
214
+ // 跨机器照抄 profile 名会被宿主门面拒绝);旧备份包无该字段 → 落 home 层(与改造前一致)
215
+ await ctx.target.patchFile.applyPatchChanges(resolveWriteLayer(prompt.sourceFile, ctx.target.profile), [
198
216
  { lineId: ref, raw, action: 'insert' },
199
217
  ]);
200
218
  return { ok: true, needsRestart: true, message: msg('adapter.promptCreated', { name: prompt.name, ref }) };
201
219
  }
202
220
  // Update / Conflict(useImported):合并进目标行 config
203
- const lines = await ctx.target.patchFile.readPatchLines(USER_PATCH_FILE);
221
+ // issue #71:先定位原行所在层(两层都没有 → home 层),再读该层的原行合并 —— 绝不把
222
+ // profile 层的行搬到 home 层(同一 lineId 两层各一份会让 DSH 的合并语义变得不可预期)
223
+ const file = await locatePatchLineLayer(ctx.target.patchFile, ctx.target.profile, ref);
224
+ const lines = await ctx.target.patchFile.readPatchLines(file);
204
225
  const line = lines.find((l) => l.lineId === ref);
205
226
  if (!line) return { ok: false, message: msg('adapter.patchLineMissing', { ref }) };
206
227
  const newRaw = mergePromptIntoLine(line.raw, prompt);
207
- await ctx.target.patchFile.applyPatchChanges(USER_PATCH_FILE, [
228
+ await ctx.target.patchFile.applyPatchChanges(file, [
208
229
  { lineId: ref, raw: newRaw, action: 'update' },
209
230
  ]);
210
231
  return { ok: true, needsRestart: true, message: msg('adapter.promptWritten', { name: prompt.name, ref }) };
@@ -124,3 +124,19 @@ test('providers: 多 route 共享 secrets 引用不触发强化 scanner 循环
124
124
  assert.equal(sanitizedProviders['openai']?.['apiKeyEnv'], 'OPENAI_API_KEY', '引用字段 apiKeyEnv 保留');
125
125
  assert.equal(sanitizedProviders['openai']?.['baseURL'], 'https://api.openai.com', '非敏感字段保留');
126
126
  });
127
+
128
+ /* ---------------- issue #73:与 settings 同型的空值判定回归 ---------------- */
129
+
130
+ test('providers: 两端 namespace 值皆空({})→ Skip(此前恒判 Create,写入是空写入)', async () => {
131
+ const adapter = new ProvidersAdapter();
132
+ const src = makeContext('win32', 'C:\\Users\\alice');
133
+ src.settings.ns.set('llm-pi-ai', { value: {}, revision: 0, secrets: [] });
134
+ const exported = await adapter.export(src, { includeSecrets: false });
135
+ const sections = new Map([['providers', exported.data]]);
136
+
137
+ const dst = makeContext('linux', '/home/bob');
138
+ dst.settings.ns.set('llm-pi-ai', { value: {}, revision: 0, secrets: [] });
139
+ const items = await adapter.analyzeImport(exported.data, makeImportContext(dst, sections));
140
+ assert.equal(items.length, 1);
141
+ assert.equal(items[0]?.kind, 'Skip', '两端皆 {} → Skip');
142
+ });
@@ -110,15 +110,16 @@ export class ProvidersAdapter implements ConfigAdapter<ProviderExportSection> {
110
110
  description: msg('adapter.providerUnregistered', { route, ns: entry.namespace }),
111
111
  severity: 'warning', target: { adapter: 'providers', ref: entry.namespace },
112
112
  });
113
+ } else if (current !== null && isDeepStrictEqual(current.value, entry.raw ?? stripEntry(entry))) {
114
+ // 值相等优先于「目标为空」——与 settings.planNamespaceItems 同一口径与顺序(issue #73)
115
+ items.push({ id, kind: 'Skip', adapter: 'providers', description: msg('adapter.providerSame', { route }), severity: 'info' });
113
116
  } else if (current === null || isEmptyValue(current.value)) {
114
- // 目标已注册但为空 → 初始化(Create)
117
+ // 目标已注册但为空**且导入值非空** → 初始化(Create)
115
118
  items.push({
116
119
  id, kind: 'Create', adapter: 'providers',
117
120
  description: msg('adapter.providerCreate', { route, ns: entry.namespace }), severity: 'info',
118
121
  target: { adapter: 'providers', ref: entry.namespace },
119
122
  });
120
- } else if (isDeepStrictEqual(current.value, entry.raw ?? stripEntry(entry))) {
121
- items.push({ id, kind: 'Skip', adapter: 'providers', description: msg('adapter.providerSame', { route }), severity: 'info' });
122
123
  } else {
123
124
  items.push({
124
125
  id, kind: 'Conflict', adapter: 'providers',