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
@@ -88,17 +88,18 @@ export class ProvidersAdapter {
88
88
  severity: 'warning', target: { adapter: 'providers', ref: entry.namespace },
89
89
  });
90
90
  }
91
+ else if (current !== null && isDeepStrictEqual(current.value, entry.raw ?? stripEntry(entry))) {
92
+ // 值相等优先于「目标为空」——与 settings.planNamespaceItems 同一口径与顺序(issue #73)
93
+ items.push({ id, kind: 'Skip', adapter: 'providers', description: msg('adapter.providerSame', { route }), severity: 'info' });
94
+ }
91
95
  else if (current === null || isEmptyValue(current.value)) {
92
- // 目标已注册但为空 → 初始化(Create)
96
+ // 目标已注册但为空**且导入值非空** → 初始化(Create)
93
97
  items.push({
94
98
  id, kind: 'Create', adapter: 'providers',
95
99
  description: msg('adapter.providerCreate', { route, ns: entry.namespace }), severity: 'info',
96
100
  target: { adapter: 'providers', ref: entry.namespace },
97
101
  });
98
102
  }
99
- else if (isDeepStrictEqual(current.value, entry.raw ?? stripEntry(entry))) {
100
- items.push({ id, kind: 'Skip', adapter: 'providers', description: msg('adapter.providerSame', { route }), severity: 'info' });
101
- }
102
103
  else {
103
104
  items.push({
104
105
  id, kind: 'Conflict', adapter: 'providers',
@@ -14,6 +14,10 @@
14
14
  * 快照/回滚路径一致);**清单来自覆写的 listRelPaths()(白名单),export() 与 preview() 都经
15
15
  * 基类 collect()**(self 目录内存在大量非配置子目录,不能像 skills/sessions 那样整体递归)。
16
16
  *
17
+ * **例外:两个调度配置的比对口径见覆写的 comparableContent()** —— sync-autosync.json /
18
+ * backup-schedule.json 内含每次执行收尾都被改写的运行态字段,整文件哈希会恒判 Conflict
19
+ * (每轮同步都重现,用户点「应用」还会把远端陈旧运行态倒灌回本机;issue #73)。
20
+ *
17
21
  * 为什么必须靠 listRelPaths() 而不是覆写 export() 来收窄(2026-10 真机报告):
18
22
  * `preview()` / `listRelPaths()` 是后来才加进基类的。本类此前只覆写 export(),于是只读预览
19
23
  * 落到基类的目录递归 —— 把整个 $DSH_HOME/dsh-config-manager(快照、config-snapshots、
@@ -63,4 +67,20 @@ export declare class SelfAdapter extends FileCollectionAdapter {
63
67
  * 同时是单元 id(self:<rel>)与 includeItems 的比对键 —— 不能随平台漂移成反斜杠。
64
68
  */
65
69
  protected listRelPaths(ctx: HostContext): Promise<RecursiveListing>;
70
+ /**
71
+ * 幂等比对口径:两个调度配置剔除**运行态字段**后比对,其余白名单文件保持整文件哈希。
72
+ *
73
+ * 为什么必要(issue #73):sync-autosync.json 由调度器在每次执行收尾改写
74
+ * lastRunAt / lastRunStatus / lastRunMessage / lastRunHistoryId / consecutiveFailures
75
+ * (src/sync/autosync-scheduler.ts),backup-schedule.json 同理(backup-scheduler)。
76
+ * 整文件 SHA-256 因此永远与远端不同 → 每轮同步都重现一条 self 分区的 Conflict,
77
+ * 且用户点「应用」会把远端的陈旧运行态倒灌回本机。
78
+ * 剔除后:**仅运行态不同 → Skip**(什么都不写,本机运行态保留);
79
+ * 配置本体(enabled / interval / startupMinIntervalMs / customSchedule / retention …)不同 → 仍是 Conflict。
80
+ *
81
+ * 其余白名单文件(sync-config / sync-selection / ui-prefs / market-config / .backup-notes)
82
+ * 已核实不含易变字段,保持内容寻址的整文件哈希。
83
+ * 解析不出 JSON 时原样返回 —— 「判不出来」按整文件哈希处理,绝不猜。
84
+ */
85
+ protected comparableContent(relativePath: string, data: Uint8Array): Uint8Array;
66
86
  }
@@ -11,6 +11,58 @@ export const SELF_CONFIG_FILES = [
11
11
  // 换机器后备份列表仍能看到手动导出时填写的备注
12
12
  'exports/.backup-notes.json',
13
13
  ];
14
+ /**
15
+ * 调度配置里的**运行态字段**:每次执行收尾都会被调度器改写,不属于「配置身份」。
16
+ * 仅比对时剔除(见 comparableContent);写盘仍写完整文件。
17
+ */
18
+ const VOLATILE_RUN_STATE_KEYS = new Set([
19
+ 'lastRunAt', 'lastRunStatus', 'lastRunMessage', 'lastRunHistoryId', 'consecutiveFailures',
20
+ ]);
21
+ /** 含运行态字段的调度配置(相对 baseDir)。 */
22
+ const SCHEDULER_CONFIG_RELS = new Set([
23
+ 'sync/sync-autosync.json',
24
+ 'sync/backup-schedule.json',
25
+ ]);
26
+ /** 递归剔除运行态键(sync-autosync v2 的 channels.git / channels.webdav 是嵌套对象)。 */
27
+ function stripRunState(node) {
28
+ if (Array.isArray(node))
29
+ return node.map(stripRunState);
30
+ if (node === null || typeof node !== 'object')
31
+ return node;
32
+ const out = {};
33
+ for (const [key, value] of Object.entries(node)) {
34
+ if (VOLATILE_RUN_STATE_KEYS.has(key))
35
+ continue;
36
+ out[key] = stripRunState(value);
37
+ }
38
+ return out;
39
+ }
40
+ /** 键排序的稳定序列化:两边键序不同(手改过的文件)时不应凭空判 Conflict。 */
41
+ function stableStringify(node) {
42
+ if (Array.isArray(node))
43
+ return '[' + node.map(stableStringify).join(',') + ']';
44
+ if (node === null || typeof node !== 'object')
45
+ return JSON.stringify(node) ?? 'null';
46
+ const entries = Object.entries(node)
47
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
48
+ return '{' + entries.map(([k, v]) => JSON.stringify(k) + ':' + stableStringify(v)).join(',') + '}';
49
+ }
50
+ /**
51
+ * 剔除运行态后的规范化 JSON 字节;解析不出 JSON(或顶层不是对象)→ null。
52
+ * **null = 「判不出可比形态」**,调用方回落原字节按整文件哈希处理 —— 不猜。
53
+ */
54
+ function canonicalizeWithoutRunState(raw) {
55
+ let parsed;
56
+ try {
57
+ parsed = JSON.parse(new TextDecoder().decode(raw));
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ if (parsed === null || typeof parsed !== 'object')
63
+ return null;
64
+ return new TextEncoder().encode(stableStringify(stripRunState(parsed)));
65
+ }
14
66
  export class SelfAdapter extends FileCollectionAdapter {
15
67
  id = 'self';
16
68
  displayName = 'Plugin Self Config';
@@ -64,5 +116,26 @@ export class SelfAdapter extends FileCollectionAdapter {
64
116
  }
65
117
  return { ...empty, paths: found };
66
118
  }
119
+ /**
120
+ * 幂等比对口径:两个调度配置剔除**运行态字段**后比对,其余白名单文件保持整文件哈希。
121
+ *
122
+ * 为什么必要(issue #73):sync-autosync.json 由调度器在每次执行收尾改写
123
+ * lastRunAt / lastRunStatus / lastRunMessage / lastRunHistoryId / consecutiveFailures
124
+ * (src/sync/autosync-scheduler.ts),backup-schedule.json 同理(backup-scheduler)。
125
+ * 整文件 SHA-256 因此永远与远端不同 → 每轮同步都重现一条 self 分区的 Conflict,
126
+ * 且用户点「应用」会把远端的陈旧运行态倒灌回本机。
127
+ * 剔除后:**仅运行态不同 → Skip**(什么都不写,本机运行态保留);
128
+ * 配置本体(enabled / interval / startupMinIntervalMs / customSchedule / retention …)不同 → 仍是 Conflict。
129
+ *
130
+ * 其余白名单文件(sync-config / sync-selection / ui-prefs / market-config / .backup-notes)
131
+ * 已核实不含易变字段,保持内容寻址的整文件哈希。
132
+ * 解析不出 JSON 时原样返回 —— 「判不出来」按整文件哈希处理,绝不猜。
133
+ */
134
+ comparableContent(relativePath, data) {
135
+ if (!SCHEDULER_CONFIG_RELS.has(relativePath))
136
+ return data;
137
+ // 回落原引用(解析失败 / 非对象):走整文件哈希,且不额外重算包内哈希
138
+ return canonicalizeWithoutRunState(data) ?? data;
139
+ }
67
140
  }
68
141
  //# sourceMappingURL=self.js.map
@@ -135,5 +135,13 @@ export declare class SessionsAdapter extends FileCollectionAdapter {
135
135
  */
136
136
  private orphanSubagentWarnings;
137
137
  applyItem(item: PlanItem, ctx: ImportContext): Promise<ApplyResult>;
138
+ /**
139
+ * 目标机上是否存在该会话单元的**任何文件**(ui-F1 的判据)。
140
+ *
141
+ * 用途:归位失败且宿主报 `unavailable` 时,区分「目录不存在(本次没写它)」与「真的搬不动」。
142
+ * 目录列表都读不到(门面抛错)按「不确定」处理 → 返回 true:保守地保留原有的硬失败上报,
143
+ * 绝不把一次真实的搬迁失败悄悄降级成成功。
144
+ */
145
+ private unitOnTargetDisk;
138
146
  finalizeApply(ctx: ImportContext): Promise<ApplyResult[]>;
139
147
  }
@@ -29,6 +29,15 @@ const MTIME_BATCH = 16;
29
29
  * 请求结束后计数随之消失(不会把会话内容或路径长期留在内存里)。
30
30
  */
31
31
  const writtenSessionFiles = new WeakMap();
32
+ /**
33
+ * 本次导入**真正写过的会话单元**(`<项目键>/<会话目录>`,与 `unitIdOf` 同一口径)。
34
+ *
35
+ * 为什么需要它(ui-F1):`finalizeApply` 拿到的是**整份分区载荷**,而用户可以在确认页
36
+ * 逐条取消勾选(`buildSelectedPlan` 只裁 `plan.items`,载荷不受影响)。按载荷做归位判定时,
37
+ * 「用户取消勾选、盘上根本没有」的会话会以 `relocateDir → unavailable` 收场,被记成**硬失败**
38
+ * → `anyFailed` → 在 `rollbackOnError=true`(向导默认、同步通道恒 true)下**整笔导入回滚**。
39
+ * 与 `writtenSessionFiles` 同样以 ImportContext 为键:宿主每个进程只建一次 adapters。 */
40
+ const writtenSessionUnits = new WeakMap();
32
41
  export class SessionsAdapter extends FileCollectionAdapter {
33
42
  id = 'sessions';
34
43
  displayName = 'Sessions';
@@ -437,10 +446,36 @@ export class SessionsAdapter extends FileCollectionAdapter {
437
446
  async applyItem(item, ctx) {
438
447
  const result = await super.applyItem(item, ctx);
439
448
  // 只累计「本次导入(同一 ImportContext)」写成功的数量 —— 见 writtenSessionFiles 的说明。
440
- if (result.ok)
449
+ if (result.ok) {
441
450
  writtenSessionFiles.set(ctx, (writtenSessionFiles.get(ctx) ?? 0) + 1);
451
+ const ref = item.target?.ref;
452
+ if (ref !== undefined && ref !== '') {
453
+ let units = writtenSessionUnits.get(ctx);
454
+ if (units === undefined) {
455
+ units = new Set();
456
+ writtenSessionUnits.set(ctx, units);
457
+ }
458
+ units.add(toPosixRel(this.unitIdOf(ref)));
459
+ }
460
+ }
442
461
  return result;
443
462
  }
463
+ /**
464
+ * 目标机上是否存在该会话单元的**任何文件**(ui-F1 的判据)。
465
+ *
466
+ * 用途:归位失败且宿主报 `unavailable` 时,区分「目录不存在(本次没写它)」与「真的搬不动」。
467
+ * 目录列表都读不到(门面抛错)按「不确定」处理 → 返回 true:保守地保留原有的硬失败上报,
468
+ * 绝不把一次真实的搬迁失败悄悄降级成成功。
469
+ */
470
+ async unitOnTargetDisk(ctx, unit) {
471
+ try {
472
+ const listing = await listFilesDetailed(ctx.target.fs, this.baseDir + '/' + unit);
473
+ return listing.paths.length > 0;
474
+ }
475
+ catch {
476
+ return true;
477
+ }
478
+ }
444
479
  async finalizeApply(ctx) {
445
480
  const data = ctx.sections.get('sessions');
446
481
  if (data === undefined || !Array.isArray(data.files))
@@ -471,7 +506,13 @@ export class SessionsAdapter extends FileCollectionAdapter {
471
506
  const mappings = ctx.pathMappings ?? [];
472
507
  const rewrite = store.rewriteLogDir;
473
508
  const reindex = store.reindexSessionHeader;
509
+ // ui-F1:本次没有写过的单元(用户取消勾选 / 未进计划)不做归位与改写判定 ——
510
+ // 「目录不存在」是它的正常形态,不是失败。appliedUnits 缺省(整批都没走 applyItem,
511
+ // 例如用户取消全部会话或单测直接调用)时保持既有行为:按载荷逐单元判定。
512
+ const appliedUnits = writtenSessionUnits.get(ctx);
474
513
  for (const [unit, files] of byUnit) {
514
+ if (appliedUnits !== undefined && !appliedUnits.has(unit))
515
+ continue;
475
516
  let cwd;
476
517
  for (const file of files) {
477
518
  const name = toPosixRel(file.relativePath).split('/').pop() ?? '';
@@ -552,6 +593,11 @@ export class SessionsAdapter extends FileCollectionAdapter {
552
593
  if (moved.moved) {
553
594
  results.push({ ok: true, message: ctx.msg('import.sessionRelocated', { unit, from: current, to: expected }) });
554
595
  }
596
+ else if (moved.reason === 'unavailable' && !(await this.unitOnTargetDisk(ctx, unit))) {
597
+ // ui-F1:目录在目标机上根本不存在 ⇒ 本次没有它的数据(用户取消勾选 / 未写盘)。
598
+ // 没有可归位的东西,绝不能记硬失败(会 no-op 地触发 rollbackOnError 整体回滚)。
599
+ continue;
600
+ }
555
601
  else {
556
602
  results.push({ ok: false, message: ctx.msg('import.sessionRelocateFailed', { unit, reason: moved.reason ?? 'unavailable' }) });
557
603
  }
@@ -1,16 +1,34 @@
1
1
  import type { MsgFunc } from '../core/messages.ts';
2
2
  import type { NamespaceRecord, SettingsSection } from '../schema/types.ts';
3
- import type { ApplyResult, ConfigAdapter, ExportOptions, ExportSection, HostContext, ImportContext, PlanItem, ValidationResult } from '../core/types.ts';
3
+ import type { ApplyResult, ConfigAdapter, ExportOptions, ExportSection, HostContext, ImportContext, NamespaceInfo, PlanItem, ValidationResult } from '../core/types.ts';
4
4
  /** namespace 清单提供者:宿主从 settings.yaml 顶层 key / 插件注册表获得 */
5
5
  export type NamespaceProvider = (ctx: HostContext) => Promise<string[]> | string[];
6
6
  export declare function resolveNamespaces(namespaces: string[] | NamespaceProvider, ctx: HostContext): Promise<string[]>;
7
+ /**
8
+ * 一次读回全部 namespace 的 redacted 记录(仅当宿主实现了 `SettingsFacade.describeAll`)。
9
+ *
10
+ * 为什么需要:真实 DSH 的 `SettingsProvider.describe()` 是「无参 = 全量」的 —— 每次调用都对
11
+ * 全部注册项重跑 `schema.toJSON()` + 多次 `structuredClone` + `redactSecrets`,所以「按名单
12
+ * 逐个 describe」实际是 O(N²):真机 24 个 namespace 实测 ≈1.7 s,而一次全量只需 ≈70 ms。
13
+ * 宿主未实现 describeAll(或调用抛错)时返回 `null`,调用方退回逐名 describe —— 与改造前逐字一致。
14
+ */
15
+ export declare function readNamespaceMap(ctx: HostContext): Promise<Map<string, NamespaceInfo> | null>;
16
+ /**
17
+ * 从全量 Map 取单个 namespace;缺失时抛出与逐名 `describe()` **逐字相同**的错误
18
+ * (`namespace not found: <name>`),保证快慢两条路径的告警文案与条目一致。
19
+ */
20
+ export declare function namespaceFromMap(all: Map<string, NamespaceInfo>, name: string): NamespaceInfo;
7
21
  /** 批量读取 namespace 的 redacted 记录(settings/ui 共用;单项失败跳过并告警) */
8
22
  export declare function collectNamespaceRecords(ctx: HostContext, names: string[], warnings: string[]): Promise<Record<string, NamespaceRecord>>;
9
23
  /** 值是否“空”(目标已注册但从未配置 → 视为待初始化,Create 而非 Conflict) */
10
24
  export declare function isEmptyValue(v: unknown): boolean;
11
- /** 对比导入记录与目标当前值:不存在 → Create;一致 → Skip;不同 → Conflict;
25
+ /** 对比导入记录与目标当前值:一致(含两端皆空)→ Skip;目标为空且导入非空 → Create;其余不同 → Conflict;
12
26
  * 目标端 describe 抛错(命名空间未注册:提供它的插件在目标未激活/未安装)→ MissingDependency(§15 依赖检测),
13
- * 此时 Create/Update 必然失败,标记为“需注意”让导入继续,而不是整体失败。纯计算,零写入。 */
27
+ * 此时 Create/Update 必然失败,标记为“需注意”让导入继续,而不是整体失败。纯计算,零写入。
28
+ *
29
+ * **判定顺序不得回退**(issue #73):一致判定必须排在「目标为空」之前 —— isEmptyValue({}) 为真,
30
+ * 反序会把「{} 覆盖 {}」判成 Create,而写入 {} 是空写入 no-op,下一轮 describe 仍返回 {},
31
+ * 于是每次同步都重现同一项,永不收敛。 */
14
32
  export declare function planNamespaceItems(data: Record<string, NamespaceRecord>, adapter: 'settings' | 'ui', ctx: ImportContext): Promise<PlanItem[]>;
15
33
  /** 写入单个 namespace(settings/ui 共用):读时锁 + expectedRevision 乐观锁,防覆盖并发修改 */
16
34
  export declare function applyNamespaceItem(item: PlanItem, sectionId: 'settings' | 'ui', ctx: ImportContext): Promise<ApplyResult>;
@@ -14,12 +14,44 @@ import { validateJsonSection } from './json-section.js';
14
14
  export async function resolveNamespaces(namespaces, ctx) {
15
15
  return typeof namespaces === 'function' ? namespaces(ctx) : namespaces;
16
16
  }
17
+ /**
18
+ * 一次读回全部 namespace 的 redacted 记录(仅当宿主实现了 `SettingsFacade.describeAll`)。
19
+ *
20
+ * 为什么需要:真实 DSH 的 `SettingsProvider.describe()` 是「无参 = 全量」的 —— 每次调用都对
21
+ * 全部注册项重跑 `schema.toJSON()` + 多次 `structuredClone` + `redactSecrets`,所以「按名单
22
+ * 逐个 describe」实际是 O(N²):真机 24 个 namespace 实测 ≈1.7 s,而一次全量只需 ≈70 ms。
23
+ * 宿主未实现 describeAll(或调用抛错)时返回 `null`,调用方退回逐名 describe —— 与改造前逐字一致。
24
+ */
25
+ export async function readNamespaceMap(ctx) {
26
+ const describeAll = ctx.settings.describeAll;
27
+ if (typeof describeAll !== 'function')
28
+ return null;
29
+ try {
30
+ const all = await describeAll.call(ctx.settings, { redactSecrets: true });
31
+ return new Map(all.map((d) => [d.ns, d.info]));
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
37
+ /**
38
+ * 从全量 Map 取单个 namespace;缺失时抛出与逐名 `describe()` **逐字相同**的错误
39
+ * (`namespace not found: <name>`),保证快慢两条路径的告警文案与条目一致。
40
+ */
41
+ export function namespaceFromMap(all, name) {
42
+ const info = all.get(name);
43
+ if (info === undefined)
44
+ throw new Error(`namespace not found: ${name}`);
45
+ return info;
46
+ }
17
47
  /** 批量读取 namespace 的 redacted 记录(settings/ui 共用;单项失败跳过并告警) */
18
48
  export async function collectNamespaceRecords(ctx, names, warnings) {
19
49
  const namespaces = {};
50
+ // 快路径:宿主能一次读回全部 namespace 时只读一趟;否则逐名读(见 readNamespaceMap)
51
+ const all = names.length > 0 ? await readNamespaceMap(ctx) : null;
20
52
  for (const name of names) {
21
53
  try {
22
- const info = await ctx.settings.describe(name, { redactSecrets: true });
54
+ const info = all !== null ? namespaceFromMap(all, name) : await ctx.settings.describe(name, { redactSecrets: true });
23
55
  namespaces[name] = {
24
56
  value: info.value,
25
57
  base: info.base,
@@ -42,9 +74,13 @@ export function isEmptyValue(v) {
42
74
  return Object.keys(v).length === 0;
43
75
  return false;
44
76
  }
45
- /** 对比导入记录与目标当前值:不存在 → Create;一致 → Skip;不同 → Conflict;
77
+ /** 对比导入记录与目标当前值:一致(含两端皆空)→ Skip;目标为空且导入非空 → Create;其余不同 → Conflict;
46
78
  * 目标端 describe 抛错(命名空间未注册:提供它的插件在目标未激活/未安装)→ MissingDependency(§15 依赖检测),
47
- * 此时 Create/Update 必然失败,标记为“需注意”让导入继续,而不是整体失败。纯计算,零写入。 */
79
+ * 此时 Create/Update 必然失败,标记为“需注意”让导入继续,而不是整体失败。纯计算,零写入。
80
+ *
81
+ * **判定顺序不得回退**(issue #73):一致判定必须排在「目标为空」之前 —— isEmptyValue({}) 为真,
82
+ * 反序会把「{} 覆盖 {}」判成 Create,而写入 {} 是空写入 no-op,下一轮 describe 仍返回 {},
83
+ * 于是每次同步都重现同一项,永不收敛。 */
48
84
  export async function planNamespaceItems(data, adapter, ctx) {
49
85
  const msg = ctx.msg;
50
86
  const items = [];
@@ -67,16 +103,17 @@ export async function planNamespaceItems(data, adapter, ctx) {
67
103
  severity: 'warning', target: { adapter, ref: name },
68
104
  });
69
105
  }
106
+ else if (current !== null && isDeepStrictEqual(current.value, rec.value)) {
107
+ // 值相等优先于「目标为空」:两端皆空(如注册后从未配置的 {})必须 Skip(issue #73)。
108
+ items.push({ id, kind: 'Skip', adapter, description: msg('adapter.settingsSame', { name }), severity: 'info' });
109
+ }
70
110
  else if (current === null || isEmptyValue(current.value)) {
71
- // 目标已注册但从未配置(空值)→ 初始化(Create)
111
+ // 目标已注册但从未配置(空值)**且导入值非空** → 初始化(Create)
72
112
  items.push({
73
113
  id, kind: 'Create', adapter, description: msg('adapter.settingsCreate', { name }), severity: 'info',
74
114
  target: { adapter, ref: name },
75
115
  });
76
116
  }
77
- else if (isDeepStrictEqual(current.value, rec.value)) {
78
- items.push({ id, kind: 'Skip', adapter, description: msg('adapter.settingsSame', { name }), severity: 'info' });
79
- }
80
117
  else {
81
118
  items.push({
82
119
  id, kind: 'Conflict', adapter, description: msg('adapter.settingsDiff', { name }),
@@ -1,13 +1,45 @@
1
+ import { FileCollectionAdapter } from './file-collection.ts';
2
+ import type { HostContext, SkillDefinitionView } from '../core/types.ts';
3
+ import type { RecursiveListing } from '../utils/recursive-walk.ts';
4
+ /** 技能服务技能的虚拟路径(相对 baseDir):`<name>/SKILL.md`。 */
5
+ export declare function skillServiceRel(name: string): string;
1
6
  /**
2
- * skills 分区 adapter(设计 §3.3):
3
- * 数据源 = ~/.dsh/skills/(flat .md + 目录 bundle;dsh-skill-filesystem 发现)。
4
- * 仅迁移用户主目录技能;~/.agents/skills、项目级技能默认不迁(研究报告 §2.2)。
7
+ * 由技能定义重建 `SKILL.md` 文本。
8
+ *
9
+ * 字段口径与 dsh-skill-filesystem 的解析器逐一对齐(多写一个键就会让 DSH 报
10
+ * 「unsupported」并**忽略整个技能**,少写一个键则会丢失调用策略):
11
+ * - 必需:`name` / `description`;
12
+ * - 可选:`whenToUse`;
13
+ * - 调用策略**只接受** `disable-model-invocation` / `user-invocable`
14
+ * (`modelInvocable` / `userInvocable` 会被判为 legacy 键并拒绝)。
5
15
  */
6
- import { FileCollectionAdapter } from './file-collection.ts';
16
+ export declare function buildSkillFile(def: SkillDefinitionView): string;
7
17
  export declare class SkillsAdapter extends FileCollectionAdapter {
8
18
  readonly id: 'skills';
9
19
  readonly displayName: string;
10
20
  readonly defaultIncluded: boolean;
11
21
  readonly portability: import("../schema/section-registry.ts").SectionPortability;
12
22
  readonly baseDir = "skills";
23
+ /** 本趟由技能服务贡献的技能定义(相对路径 → 定义)。每次 collect 前重建,避免跨趟串味。 */
24
+ private serviceSkills;
25
+ /**
26
+ * 枚举 = 目录扫描 ∪ 技能服务清单。
27
+ *
28
+ * 服务不可用(旧宿主 / 无 skills 服务)时**完全退回**目录扫描:一个告警都不多加,
29
+ * 行为与改造前逐项一致(file-collection.test.ts 钉住「无服务时不产生新告警」)。
30
+ */
31
+ protected listRelPaths(ctx: HostContext): Promise<RecursiveListing>;
32
+ /** 分区内相对路径 → 枚举清单口径(含 baseDir;本分区 baseDir 恒为 `skills`,没有根分区形态)。 */
33
+ private listRelPathOf;
34
+ /**
35
+ * 读成员:先看本趟的服务清单(虚拟 `<name>/SKILL.md`),命中则用重建文本;
36
+ * 否则走基类的文件读(含 statSize 快路径)。
37
+ *
38
+ * 两条路都必须给出**同一个体积口径**(UTF-8 字节数),否则字节闸门在预览与导出之间分叉。
39
+ */
40
+ protected readMember(ctx: HostContext, rel: string, mode: 'content' | 'size', statTimes?: Map<string, number>): Promise<{
41
+ data: Uint8Array;
42
+ sizeBytes: number;
43
+ contentHash: string;
44
+ }>;
13
45
  }
@@ -1,10 +1,153 @@
1
1
  /**
2
- * skills 分区 adapter(设计 §3.3):
3
- * 数据源 = ~/.dsh/skills/(flat .md + 目录 bundle;dsh-skill-filesystem 发现)。
4
- * 仅迁移用户主目录技能;~/.agents/skills、项目级技能默认不迁(研究报告 §2.2)。
2
+ * skills 分区 adapter(设计 §3.3;issue #71 重做数据源)。
3
+ *
4
+ * 数据源**两路合并**:
5
+ * 1. **文件目录** `$DSH_HOME/skills`(flat `.md` + `<name>/SKILL.md` bundle);
6
+ * 2. **技能服务** `ctx.skills`(DSH 的 `@deepseek-ai/dsh-skill` 注册表)—— 这是**唯一**能
7
+ * 看见「外壳技能」的通道。
8
+ *
9
+ * 为什么必须加第 2 路(真机报告「备份不到外壳 skills」):技能 provider 由插件在 apply 时注册
10
+ * (`ctx.skills.registerProvider`),来源可以是
11
+ * - pnpm 装进 profile 的插件包(`profiles/<p>/node_modules` 下任意层级的 `SKILL.md`,如 dsh-reverse-skill);
12
+ * - 插件配置项 `customSkillDirs` 指向的任意目录;
13
+ * - 内置目录 `DSH_BUNDLED_SKILL_DIR`。
14
+ * 这些技能**一个都不在** `$DSH_HOME/skills` 里,只扫目录的结果就是「技能分区恒为空」。
15
+ *
16
+ * 合并语义(与 `ctx.skills` 自己的层级语义一致:global 打底、scope 链覆盖):
17
+ * - 服务里的技能统一映射成虚拟路径 `<name>/SKILL.md`(DSH 自己的目录形态);
18
+ * - 同一相对路径**文件系统优先**(用户磁盘上的原文是事实源,服务可能已做归一化);
19
+ * - 单元 id 仍是首个路径段 = 技能名(`unitIdOf` 缺省规则),因此选择器上「一个技能一个勾选项」,
20
+ * 且与导入侧 `PlanItem.unitId` 自动对齐。
21
+ *
22
+ * 仅迁移用户可迁移的技能:项目级技能(`.agents/skills`、仓库内 skills 目录)不在本分区射程内
23
+ * (研究报告 §2.2 同策略)。
5
24
  */
25
+ import path from 'node:path';
6
26
  import { FileCollectionAdapter } from './file-collection.js';
27
+ import { sha256Hex } from '../utils/hashing.js';
28
+ import { isPathSafe, normalizePath } from '../utils/paths.js';
7
29
  import { sectionMeta } from '../schema/section-registry.js';
30
+ /** 技能服务技能的虚拟路径(相对 baseDir):`<name>/SKILL.md`。 */
31
+ export function skillServiceRel(name) {
32
+ return normalizePath(path.join(name, 'SKILL.md'));
33
+ }
34
+ /**
35
+ * YAML 标量编码(issue #71 真机验收发现的缺陷修复)。
36
+ *
37
+ * 原实现只做单引号转义(`'` → `''`),**值里含换行时会写出跨行的单引号标量**:真机
38
+ * `binary-diff` 的 `description` 是块标量(解析后含换行),落盘即非法 YAML,外壳
39
+ * `dsh-skill-filesystem` 会 `skill file … ignored: invalid YAML frontmatter` 把**整个技能丢掉**
40
+ * ——比不备份更糟。三档策略(都不 import `js-yaml`,adapters 层不该依赖它):
41
+ * 1. 能安全缩进的多行值 → 块标量 `|`(可读、便于 diff);
42
+ * 2. 含换行 / 回车 / 控制字符的值 → 双引号 + 转义(转义表与 js-yaml dumper 一致);
43
+ * 3. 其余单行值 → 单引号(与原行为一致,`'` 用 `''` 转义)。
44
+ * 三档都保证解析回来与原字符串**逐字符相等**。
45
+ */
46
+ const NAMED_ESCAPES = {
47
+ '\0': '\\0',
48
+ '\x07': '\\a',
49
+ '\b': '\\b',
50
+ '\t': '\\t',
51
+ '\n': '\\n',
52
+ '\v': '\\v',
53
+ '\f': '\\f',
54
+ '\r': '\\r',
55
+ '\x1b': '\\e',
56
+ '"': '\\"',
57
+ '\\': '\\\\',
58
+ '\x85': '\\N',
59
+ '\xa0': '\\_',
60
+ '\u2028': '\\L',
61
+ '\u2029': '\\P',
62
+ };
63
+ /** 该字符必须转义才能安全出现在 YAML 标量里(控制字符 / DEL / C1 / BOM / 代理对)。 */
64
+ function needsEscape(ch) {
65
+ const code = ch.codePointAt(0) ?? 0;
66
+ return (code < 0x20 ||
67
+ code === 0x7f ||
68
+ (code >= 0x80 && code <= 0xa0) ||
69
+ code === 0xfeff ||
70
+ code === 0xfffe ||
71
+ code === 0xffff ||
72
+ (code >= 0xd800 && code <= 0xdfff));
73
+ }
74
+ /** 双引号标量(唯一能无损表达任意字符串的 YAML 标量形态)。 */
75
+ function doubleQuoteYaml(value) {
76
+ let out = '"';
77
+ for (const ch of value) {
78
+ const named = NAMED_ESCAPES[ch];
79
+ if (named !== undefined)
80
+ out += named;
81
+ else if (needsEscape(ch)) {
82
+ const code = ch.codePointAt(0) ?? 0;
83
+ const hex = code.toString(16).toUpperCase();
84
+ out += code <= 0xff ? `\\x${hex.padStart(2, '0')}` : `\\u${hex.padStart(4, '0')}`;
85
+ }
86
+ else
87
+ out += ch;
88
+ }
89
+ return `${out}"`;
90
+ }
91
+ /**
92
+ * 该值能否写成块标量 `|`(缩进 2 空格)。
93
+ *
94
+ * 块标量默认 clip chomping:**恰好一个**尾换行会被还原,没有尾换行会多补一个、多个尾换行会被裁到
95
+ * 一个(实测 `a\n\n` → `a\n`)⇒ 只对「以恰好一个换行结尾」的值无损。行内空行本身安全
96
+ * (实测 `a\n\nb\n` 精确往返);含回车、非空行有首尾空白、或含需转义字符的仍走双引号。
97
+ */
98
+ function canUseBlockScalar(value) {
99
+ if (!value.endsWith('\n') || value.endsWith('\n\n'))
100
+ return false;
101
+ if (value.includes('\r'))
102
+ return false;
103
+ for (const line of value.split('\n')) {
104
+ if (line === '')
105
+ continue;
106
+ if (/^[ \t]|[ \t]$/.test(line))
107
+ return false;
108
+ for (const ch of line)
109
+ if (needsEscape(ch))
110
+ return false;
111
+ }
112
+ return true;
113
+ }
114
+ /** 值 → YAML 标量(三档策略见上方注释)。 */
115
+ function yamlScalar(value) {
116
+ if (canUseBlockScalar(value)) {
117
+ // 尾换行交给块标量的 clip chomping 还原(见 canUseBlockScalar 注释),故先摘掉
118
+ const body = value
119
+ .slice(0, -1)
120
+ .split('\n')
121
+ .map((line) => ` ${line}`)
122
+ .join('\n');
123
+ return `|\n${body}`;
124
+ }
125
+ if (/[\n\r]/.test(value) || [...value].some(needsEscape))
126
+ return doubleQuoteYaml(value);
127
+ return `'${value.replace(/'/g, "''")}'`;
128
+ }
129
+ /**
130
+ * 由技能定义重建 `SKILL.md` 文本。
131
+ *
132
+ * 字段口径与 dsh-skill-filesystem 的解析器逐一对齐(多写一个键就会让 DSH 报
133
+ * 「unsupported」并**忽略整个技能**,少写一个键则会丢失调用策略):
134
+ * - 必需:`name` / `description`;
135
+ * - 可选:`whenToUse`;
136
+ * - 调用策略**只接受** `disable-model-invocation` / `user-invocable`
137
+ * (`modelInvocable` / `userInvocable` 会被判为 legacy 键并拒绝)。
138
+ */
139
+ export function buildSkillFile(def) {
140
+ const lines = ['---', `name: ${yamlScalar(def.name)}`, `description: ${yamlScalar(def.description)}`];
141
+ if (def.whenToUse !== undefined && def.whenToUse !== '')
142
+ lines.push(`whenToUse: ${yamlScalar(def.whenToUse)}`);
143
+ const invocation = def.invocation;
144
+ if (invocation !== undefined && invocation.modelInvocable === false)
145
+ lines.push('disable-model-invocation: true');
146
+ if (invocation !== undefined && invocation.userInvocable === false)
147
+ lines.push('user-invocable: false');
148
+ // 正文原样保留(服务返回的 content 已是 frontmatter 之后的正文;不 trim,避免丢用户排版)
149
+ return `${lines.join('\n')}\n---\n\n${def.content}`;
150
+ }
8
151
  export class SkillsAdapter extends FileCollectionAdapter {
9
152
  id = 'skills';
10
153
  // 元数据唯一来源 = 注册表(t31):不再与 ui/export-flow.ts 的导出目录各写一份
@@ -12,5 +155,81 @@ export class SkillsAdapter extends FileCollectionAdapter {
12
155
  defaultIncluded = sectionMeta('skills').defaultIncluded;
13
156
  portability = sectionMeta('skills').portability;
14
157
  baseDir = 'skills';
158
+ /** 本趟由技能服务贡献的技能定义(相对路径 → 定义)。每次 collect 前重建,避免跨趟串味。 */
159
+ serviceSkills = new Map();
160
+ /**
161
+ * 枚举 = 目录扫描 ∪ 技能服务清单。
162
+ *
163
+ * 服务不可用(旧宿主 / 无 skills 服务)时**完全退回**目录扫描:一个告警都不多加,
164
+ * 行为与改造前逐项一致(file-collection.test.ts 钉住「无服务时不产生新告警」)。
165
+ */
166
+ async listRelPaths(ctx) {
167
+ const listing = await super.listRelPaths(ctx);
168
+ this.serviceSkills = new Map();
169
+ if (ctx.skills === undefined)
170
+ return listing;
171
+ let summaries;
172
+ try {
173
+ summaries = await ctx.skills.list();
174
+ }
175
+ catch (err) {
176
+ ctx.log.warn(`技能服务列举失败(该路技能未进备份): ${err instanceof Error ? err.message : String(err)}`);
177
+ return listing;
178
+ }
179
+ const extra = [];
180
+ // 目录清单里的是「含 baseDir 的路径」(`skills/<name>/SKILL.md`),服务侧算的是分区内相对路径,
181
+ // 必须先归一到同一口径再比,否则同路径判不出重合 → 磁盘原文会被服务重建文本挤掉。
182
+ const onDisk = new Set(listing.paths.map((p) => this.relPathOf(p)));
183
+ for (const summary of summaries) {
184
+ // 名字要能当路径用:DSH 的 SKILL_NAME 是 [a-z0-9-],这里再做一道通用安全校验(拒 `..`/绝对路径)
185
+ if (typeof summary.name !== 'string' || !isPathSafe(summary.name) || summary.name.includes('/'))
186
+ continue;
187
+ const rel = skillServiceRel(summary.name);
188
+ if (onDisk.has(rel))
189
+ continue; // 文件系统优先
190
+ let definition;
191
+ try {
192
+ definition = await ctx.skills.get(summary.name);
193
+ }
194
+ catch (err) {
195
+ ctx.log.warn(`技能 ${summary.name} 读取失败(未进备份): ${err instanceof Error ? err.message : String(err)}`);
196
+ continue;
197
+ }
198
+ if (definition === undefined || typeof definition.content !== 'string')
199
+ continue;
200
+ this.serviceSkills.set(rel, definition);
201
+ // ui-F5:listing.paths 是「含 baseDir」的路径(与磁盘清单同口径),服务侧算出来的是
202
+ // **分区内相对路径** —— 必须补回 baseDir 前缀再入列。否则技能名恰好等于 baseDir(`skills`,
203
+ // 合法技能名)时,虚拟路径 skills/SKILL.md 会在 collect() 里被 relPathOf() 二次剥前缀成
204
+ // SKILL.md:单元 id 变了、readMember 查不到服务定义 → 回落磁盘读 → ENOENT → 整个 skills
205
+ // 分区预览/导出失败(导出侧被 exporter 整块剔除)。
206
+ extra.push(this.listRelPathOf(rel));
207
+ }
208
+ if (extra.length === 0)
209
+ return listing;
210
+ return { ...listing, paths: [...listing.paths, ...extra] };
211
+ }
212
+ /** 分区内相对路径 → 枚举清单口径(含 baseDir;本分区 baseDir 恒为 `skills`,没有根分区形态)。 */
213
+ listRelPathOf(rel) {
214
+ return normalizePath(`${this.baseDir}/${rel}`);
215
+ }
216
+ /**
217
+ * 读成员:先看本趟的服务清单(虚拟 `<name>/SKILL.md`),命中则用重建文本;
218
+ * 否则走基类的文件读(含 statSize 快路径)。
219
+ *
220
+ * 两条路都必须给出**同一个体积口径**(UTF-8 字节数),否则字节闸门在预览与导出之间分叉。
221
+ */
222
+ async readMember(ctx, rel, mode, statTimes) {
223
+ const definition = this.serviceSkills.get(this.relPathOf(rel));
224
+ if (definition === undefined)
225
+ return super.readMember(ctx, rel, mode, statTimes);
226
+ const text = buildSkillFile(definition);
227
+ const data = new TextEncoder().encode(text);
228
+ if (mode === 'size') {
229
+ // 预览:不建哈希(与基类的 size 模式同语义),但体积必须精确
230
+ return { data: new Uint8Array(0), sizeBytes: data.byteLength, contentHash: '' };
231
+ }
232
+ return { data, sizeBytes: data.byteLength, contentHash: sha256Hex(data) };
233
+ }
15
234
  }
16
235
  //# sourceMappingURL=skills.js.map