dsh-config-manager 0.1.71 → 0.1.72

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 (548) hide show
  1. package/README.md +111 -96
  2. package/README.zh-CN.md +207 -86
  3. package/lib/adapters/agent-instructions.js +1 -1
  4. package/lib/adapters/file-collection.js +1 -1
  5. package/lib/adapters/index.d.ts +1 -1
  6. package/lib/adapters/index.js +1 -1
  7. package/lib/adapters/link-report.d.ts +1 -0
  8. package/lib/adapters/link-report.js +27 -3
  9. package/lib/adapters/messages.d.ts +18 -0
  10. package/lib/adapters/messages.js +50 -0
  11. package/lib/adapters/plugin-files.js +3 -2
  12. package/lib/adapters/plugins.js +4 -3
  13. package/lib/adapters/prompts.d.ts +1 -0
  14. package/lib/adapters/prompts.js +7 -1
  15. package/lib/adapters/self.d.ts +7 -1
  16. package/lib/adapters/self.js +1 -1
  17. package/lib/adapters/sessions.d.ts +1 -0
  18. package/lib/adapters/sessions.js +4 -2
  19. package/lib/adapters/ui.d.ts +2 -2
  20. package/lib/adapters/ui.js +25 -25
  21. package/lib/adapters/workspaces.d.ts +1 -0
  22. package/lib/adapters/workspaces.js +7 -3
  23. package/lib/cli/actions.d.ts +9 -1
  24. package/lib/cli/actions.js +5 -2
  25. package/lib/cli/help.js +23 -10
  26. package/lib/cli/index.d.ts +23 -0
  27. package/lib/cli/index.js +90 -32
  28. package/lib/cli/sessions-inspect.js +3 -1
  29. package/lib/cli/sessions-repair.js +25 -6
  30. package/lib/cli/web/page.d.ts +12 -0
  31. package/lib/cli/web/page.js +21 -12
  32. package/lib/cli/web/routes.js +2 -2
  33. package/lib/cli/web/server.js +4 -4
  34. package/lib/client.d.ts +1349 -490
  35. package/lib/client.js +43047 -36738
  36. package/lib/core/backup-plan.js +30 -17
  37. package/lib/core/backup-verify.js +24 -23
  38. package/lib/core/backup.d.ts +5 -0
  39. package/lib/core/backup.js +51 -25
  40. package/lib/core/boot-rescue.js +4 -3
  41. package/lib/core/boot-safety.js +2 -1
  42. package/lib/core/command-rollback.d.ts +178 -0
  43. package/lib/core/command-rollback.js +289 -0
  44. package/lib/core/consult-source.js +7 -6
  45. package/lib/core/disk-usage.d.ts +74 -10
  46. package/lib/core/disk-usage.js +67 -4
  47. package/lib/core/engine-locale.d.ts +87 -0
  48. package/lib/core/engine-locale.js +125 -0
  49. package/lib/core/exporter.js +3 -3
  50. package/lib/core/fork-reconcile.d.ts +128 -0
  51. package/lib/core/fork-reconcile.js +150 -0
  52. package/lib/core/journal.js +6 -5
  53. package/lib/core/local-plugin-host.js +3 -2
  54. package/lib/core/local-plugin-pack.js +11 -6
  55. package/lib/core/messages-core.d.ts +292 -0
  56. package/lib/core/messages-core.js +615 -0
  57. package/lib/core/messages.d.ts +60 -2
  58. package/lib/core/messages.js +116 -13
  59. package/lib/core/migration-consult.js +21 -20
  60. package/lib/core/migration-history.js +19 -5
  61. package/lib/core/model-tools.d.ts +0 -22
  62. package/lib/core/model-tools.js +50 -27
  63. package/lib/core/phase3-host.js +30 -5
  64. package/lib/core/plugin-cli.js +18 -19
  65. package/lib/core/reconcile.d.ts +2 -14
  66. package/lib/core/reconcile.js +87 -17
  67. package/lib/core/recovery-orchestrator.js +19 -18
  68. package/lib/core/reinstall.d.ts +4 -0
  69. package/lib/core/reinstall.js +26 -21
  70. package/lib/core/restore.js +20 -17
  71. package/lib/core/rollback-ignore-rules.d.ts +31 -0
  72. package/lib/core/rollback-ignore-rules.js +140 -0
  73. package/lib/core/rollback-point.d.ts +1208 -0
  74. package/lib/core/rollback-point.js +2993 -0
  75. package/lib/core/run-registry.js +6 -5
  76. package/lib/core/session-health.d.ts +15 -0
  77. package/lib/core/session-meta.d.ts +42 -2
  78. package/lib/core/session-meta.js +140 -1
  79. package/lib/core/session-title.d.ts +28 -0
  80. package/lib/core/session-title.js +185 -0
  81. package/lib/core/startup-barrier.d.ts +24 -0
  82. package/lib/core/startup-barrier.js +22 -0
  83. package/lib/core/transaction-coordinator.js +4 -3
  84. package/lib/core/types.d.ts +18 -1
  85. package/lib/core/validator.js +5 -4
  86. package/lib/core/verify-recovery.js +31 -30
  87. package/lib/foreign/bundle.js +3 -2
  88. package/lib/foreign/continue.js +2 -1
  89. package/lib/foreign/crush.js +2 -1
  90. package/lib/foreign/gemini.js +2 -1
  91. package/lib/foreign/goose.js +2 -1
  92. package/lib/foreign/kilocode.js +2 -1
  93. package/lib/foreign/kimi.js +2 -1
  94. package/lib/foreign/messages.d.ts +15 -0
  95. package/lib/foreign/messages.js +40 -0
  96. package/lib/foreign/mimocode.js +2 -1
  97. package/lib/foreign/openclaw.js +2 -1
  98. package/lib/foreign/opencode.js +2 -1
  99. package/lib/foreign/pi.js +2 -1
  100. package/lib/foreign/qoder.js +2 -1
  101. package/lib/foreign/qwen.js +2 -1
  102. package/lib/foreign/registry.js +7 -6
  103. package/lib/foreign/teleagent.js +2 -1
  104. package/lib/foreign/workbuddy.js +2 -1
  105. package/lib/foreign/zcode.js +2 -1
  106. package/lib/foreign/zed.js +2 -1
  107. package/lib/index.d.ts +223 -7
  108. package/lib/index.js +729 -54
  109. package/lib/market/git-file-writer.js +20 -19
  110. package/lib/market/github-repos.js +35 -19
  111. package/lib/market/index-parser.js +31 -30
  112. package/lib/market/messages-market.d.ts +138 -0
  113. package/lib/market/messages-market.js +305 -0
  114. package/lib/market/my-repo.d.ts +2 -2
  115. package/lib/market/my-repo.js +32 -29
  116. package/lib/market/prepare.js +18 -15
  117. package/lib/market/security.js +20 -19
  118. package/lib/market/types.js +2 -1
  119. package/lib/market/url.js +2 -1
  120. package/lib/migrations/index.js +6 -5
  121. package/lib/migrations/messages-migrations.d.ts +32 -0
  122. package/lib/migrations/messages-migrations.js +58 -0
  123. package/lib/profiles/dsh-profile-launcher.js +2 -1
  124. package/lib/profiles/dsh-profile-runtime.js +2 -1
  125. package/lib/profiles/messages.d.ts +7 -0
  126. package/lib/profiles/messages.js +27 -0
  127. package/lib/routes/backup.js +18 -2
  128. package/lib/routes/import.js +3 -3
  129. package/lib/routes/index.d.ts +1 -1
  130. package/lib/routes/kit.d.ts +3 -2
  131. package/lib/routes/kit.js +6 -1
  132. package/lib/routes/market.js +1 -1
  133. package/lib/routes/messages.d.ts +42 -0
  134. package/lib/routes/messages.js +100 -0
  135. package/lib/routes/profiles.js +12 -6
  136. package/lib/routes/recovery.js +378 -114
  137. package/lib/routes/snapshots.js +51 -4
  138. package/lib/routes/sync.js +68 -16
  139. package/lib/schema/config.d.ts +11 -1
  140. package/lib/schema/config.js +52 -20
  141. package/lib/schema/index.d.ts +8 -6
  142. package/lib/schema/index.js +8 -6
  143. package/lib/schema/manifest.js +19 -18
  144. package/lib/schema/messages.d.ts +47 -0
  145. package/lib/schema/messages.js +110 -0
  146. package/lib/schema/section-registry.d.ts +41 -14
  147. package/lib/schema/section-registry.js +35 -29
  148. package/lib/schema/versions.js +6 -5
  149. package/lib/security/encryption.js +16 -15
  150. package/lib/security/integrity.js +8 -7
  151. package/lib/security/messages.d.ts +30 -0
  152. package/lib/security/messages.js +77 -0
  153. package/lib/security/redaction.js +12 -0
  154. package/lib/security/secret-scanner.js +16 -13
  155. package/lib/security/vault.js +2 -1
  156. package/lib/security/zip-security.js +4 -3
  157. package/lib/sync/ancestor.js +2 -1
  158. package/lib/sync/autosync-config.d.ts +11 -0
  159. package/lib/sync/autosync-config.js +12 -0
  160. package/lib/sync/autosync-scheduler.d.ts +11 -1
  161. package/lib/sync/autosync-scheduler.js +115 -76
  162. package/lib/sync/backup-files.js +3 -2
  163. package/lib/sync/backup-scheduler.js +6 -5
  164. package/lib/sync/blob-store.js +2 -1
  165. package/lib/sync/git/git-transport.d.ts +55 -0
  166. package/lib/sync/git/git-transport.js +152 -2
  167. package/lib/sync/github-auth.js +10 -9
  168. package/lib/sync/layout.js +13 -12
  169. package/lib/sync/messages.d.ts +63 -0
  170. package/lib/sync/messages.js +149 -0
  171. package/lib/sync/review-queue.js +6 -5
  172. package/lib/sync/s3/messages.d.ts +7 -0
  173. package/lib/sync/s3/messages.js +16 -0
  174. package/lib/sync/s3/s3-providers.d.ts +4 -1
  175. package/lib/sync/s3/s3-providers.js +5 -5
  176. package/lib/sync/s3/s3-transport.js +1 -1
  177. package/lib/sync/s3/sigv4.js +2 -1
  178. package/lib/sync/snapshot-crypto.js +6 -5
  179. package/lib/sync/snapshot-json.js +3 -2
  180. package/lib/sync/sync-engine.d.ts +37 -15
  181. package/lib/sync/sync-engine.js +67 -37
  182. package/lib/sync/sync-history.js +7 -6
  183. package/lib/sync/sync-selection.d.ts +15 -4
  184. package/lib/sync/sync-selection.js +17 -5
  185. package/lib/sync/webdav/webdav-transport.d.ts +1 -0
  186. package/lib/sync/webdav/webdav-transport.js +4 -1
  187. package/lib/ui/artifact-view.js +4 -3
  188. package/lib/ui/commands.d.ts +1 -1
  189. package/lib/ui/commands.js +10 -0
  190. package/lib/ui/disk-usage-view.d.ts +33 -4
  191. package/lib/ui/disk-usage-view.js +29 -1
  192. package/lib/ui/errors.d.ts +1 -1
  193. package/lib/ui/errors.js +4 -3
  194. package/lib/ui/export-flow.js +24 -20
  195. package/lib/ui/foreign-view.d.ts +5 -1
  196. package/lib/ui/foreign-view.js +26 -2
  197. package/lib/ui/i18n.d.ts +65 -0
  198. package/lib/ui/i18n.js +114 -0
  199. package/lib/ui/import-wizard.d.ts +7 -0
  200. package/lib/ui/import-wizard.js +13 -10
  201. package/lib/ui/progress.d.ts +2 -2
  202. package/lib/ui/progress.js +1 -2
  203. package/lib/ui/report.d.ts +5 -5
  204. package/lib/ui/report.js +6 -6
  205. package/lib/ui/rollback-view.d.ts +756 -0
  206. package/lib/ui/rollback-view.js +724 -0
  207. package/lib/ui/selection-model.d.ts +41 -0
  208. package/lib/ui/selection-model.js +30 -0
  209. package/lib/ui/session-inventory-view.d.ts +97 -3
  210. package/lib/ui/session-inventory-view.js +76 -2
  211. package/lib/ui/session-issue-labels.d.ts +43 -0
  212. package/lib/ui/session-issue-labels.js +48 -0
  213. package/lib/ui/sync-settings-view.d.ts +33 -0
  214. package/lib/ui/sync-settings-view.js +40 -0
  215. package/lib/ui/types.d.ts +8 -3
  216. package/lib/ui/types.js +1 -1
  217. package/lib/utils/atomic-write.js +6 -5
  218. package/lib/utils/bundle-scan.d.ts +11 -1
  219. package/lib/utils/bundle-scan.js +12 -1
  220. package/lib/utils/env-lock.d.ts +10 -0
  221. package/lib/utils/env-lock.js +73 -46
  222. package/lib/utils/json.js +6 -5
  223. package/lib/utils/messages-utils.d.ts +96 -0
  224. package/lib/utils/messages-utils.js +216 -0
  225. package/lib/utils/proxy.js +18 -5
  226. package/lib/utils/recursive-walk.d.ts +22 -0
  227. package/lib/utils/recursive-walk.js +75 -1
  228. package/lib/utils/session-log-repair.js +10 -9
  229. package/lib/utils/zip.js +27 -26
  230. package/package.json +12 -2
  231. package/src/adapters/agent-instructions.ts +1 -1
  232. package/src/adapters/file-collection.test.ts +1 -0
  233. package/src/adapters/file-collection.ts +1 -1
  234. package/src/adapters/files.test.ts +27 -0
  235. package/src/adapters/index.ts +1 -1
  236. package/src/adapters/link-report.test.ts +22 -2
  237. package/src/adapters/link-report.ts +29 -4
  238. package/src/adapters/messages.ts +54 -0
  239. package/src/adapters/plugin-files.ts +3 -2
  240. package/src/adapters/plugins.ts +4 -3
  241. package/src/adapters/prompts.ts +7 -1
  242. package/src/adapters/self.test.ts +26 -0
  243. package/src/adapters/self.ts +8 -2
  244. package/src/adapters/sessions.ts +4 -2
  245. package/src/adapters/settings.test.ts +38 -1
  246. package/src/adapters/ui.test.ts +2 -2
  247. package/src/adapters/ui.ts +27 -26
  248. package/src/adapters/workspaces.ts +7 -3
  249. package/src/cli/actions.test.ts +28 -0
  250. package/src/cli/actions.ts +11 -2
  251. package/src/cli/help.ts +23 -10
  252. package/src/cli/index-guards.test.ts +193 -0
  253. package/src/cli/index.ts +106 -31
  254. package/src/cli/sessions-inspect.test.ts +15 -1
  255. package/src/cli/sessions-inspect.ts +3 -1
  256. package/src/cli/sessions-repair.test.ts +84 -0
  257. package/src/cli/sessions-repair.ts +33 -7
  258. package/src/cli/web/console-route-parity.test.ts +115 -0
  259. package/src/cli/web/page.ts +23 -12
  260. package/src/cli/web/routes.ts +2 -1
  261. package/src/cli/web/server.ts +4 -4
  262. package/src/cli/web/web.test.ts +2 -2
  263. package/src/client/ConfigManagerSection.tsx +30 -2
  264. package/src/client/about/AboutPanel.tsx +2 -2
  265. package/src/client/about/PluginUpdateDialog.tsx +1 -1
  266. package/src/client/about/ReleaseNotesDialog.tsx +2 -2
  267. package/src/client/about/about-view.test.ts +13 -3
  268. package/src/client/about/about-view.ts +10 -6
  269. package/src/client/about/release-notes-view.test.ts +14 -4
  270. package/src/client/about/release-notes-view.ts +4 -2
  271. package/src/client/api.ts +18 -6
  272. package/src/client/chat/ChatEntryView.tsx +391 -0
  273. package/src/client/chat/RollbackDiffTab.tsx +61 -0
  274. package/src/client/chat/RollbackFileIcon.tsx +96 -0
  275. package/src/client/chat/RollbackReviewDialog.tsx +591 -0
  276. package/src/client/chat/chat-entry-view.test.ts +1174 -0
  277. package/src/client/chat/chat-entry-view.ts +758 -0
  278. package/src/client/chat/chat-locales.ts +152 -0
  279. package/src/client/chat/chat-node-registration.ts +64 -0
  280. package/src/client/chat/chat-point-window.ts +173 -0
  281. package/src/client/chat/chat-slots.ts +107 -0
  282. package/src/client/chat/rollback-diff-tab.test.ts +105 -0
  283. package/src/client/chat/rollback-diff-tab.ts +164 -0
  284. package/src/client/chat/rollback-entry-view.test.ts +1469 -0
  285. package/src/client/chat/rollback-entry-view.ts +941 -0
  286. package/src/client/chat/sidebar-slots.ts +37 -0
  287. package/src/client/common/ConfirmDialog.tsx +7 -3
  288. package/src/client/common/ContentPicker.tsx +48 -10
  289. package/src/client/common/DiffTable.tsx +97 -0
  290. package/src/client/common/ErrorBanner.tsx +4 -4
  291. package/src/client/common/Icon.tsx +2 -0
  292. package/src/client/common/Modal.tsx +8 -1
  293. package/src/client/common/ProgressBar.tsx +8 -2
  294. package/src/client/common/ReportView.tsx +4 -4
  295. package/src/client/common/RunsCenter.tsx +1 -0
  296. package/src/client/common/client-route-parity.test.ts +78 -0
  297. package/src/client/common/info-hint-guard.test.ts +46 -13
  298. package/src/client/common/picker-lazy-units.test.ts +42 -0
  299. package/src/client/common/plan-text-redaction.test.ts +17 -6
  300. package/src/client/common/progress-view-edge.test.ts +8 -7
  301. package/src/client/common/progress-view.test.ts +9 -9
  302. package/src/client/common/progress-view.ts +2 -2
  303. package/src/client/common/routes.ts +28 -0
  304. package/src/client/common/ui.tsx +73 -10
  305. package/src/client/config-manager.module.css +996 -21
  306. package/src/client/css-scale-guard.test.ts +30 -0
  307. package/src/client/environment/EnvironmentPanel.tsx +11 -4
  308. package/src/client/environment/maintenance/DiskUsageCard.tsx +23 -2
  309. package/src/client/export/ExportView.tsx +13 -8
  310. package/src/client/history/HistoryPanel.tsx +4 -3
  311. package/src/client/history/history-api.ts +2 -2
  312. package/src/client/history/history-locales.ts +5 -1
  313. package/src/client/home/HomePanel.tsx +29 -8
  314. package/src/client/import/ForeignImportView.tsx +4 -5
  315. package/src/client/import/ImportWizardView.tsx +5 -3
  316. package/src/client/import/import-wizard-split.test.ts +3 -1
  317. package/src/client/import/import-wizard-steps.tsx +9 -9
  318. package/src/client/index.ts +186 -3
  319. package/src/client/library/LibraryActions.tsx +32 -7
  320. package/src/client/library/LibraryPanel.tsx +23 -3
  321. package/src/client/library/RestorePlanView.tsx +9 -66
  322. package/src/client/locales.ts +54 -5
  323. package/src/client/lucide-icons.d.ts +1 -0
  324. package/src/client/market/MarketPanel.tsx +13 -2
  325. package/src/client/market/MyConfigsView.tsx +5 -4
  326. package/src/client/market/MyConfigsWizard.tsx +10 -8
  327. package/src/client/market/market-api.test.ts +9 -8
  328. package/src/client/market/market-api.ts +2 -2
  329. package/src/client/market/market-locales.ts +3 -0
  330. package/src/client/market/market-view.test.ts +6 -6
  331. package/src/client/market/market-view.ts +3 -3
  332. package/src/client/market/my-configs-api.ts +2 -2
  333. package/src/client/market/my-configs-view.test.ts +23 -22
  334. package/src/client/market/my-configs-view.ts +8 -8
  335. package/src/client/recovery/IncompleteCopiesSection.tsx +2 -1
  336. package/src/client/recovery/RecoveryPanel.tsx +442 -283
  337. package/src/client/recovery/RollbackCard.tsx +665 -0
  338. package/src/client/recovery/RollbackDiffPane.tsx +58 -0
  339. package/src/client/recovery/RollbackFileChanges.tsx +163 -0
  340. package/src/client/recovery/incident-api.ts +2 -2
  341. package/src/client/recovery/recovery-api.ts +116 -6
  342. package/src/client/recovery/recovery-locales.test.ts +299 -0
  343. package/src/client/recovery/recovery-locales.ts +488 -149
  344. package/src/client/recovery/recovery-panel-layout.test.ts +132 -2
  345. package/src/client/recovery/rollback-file-changes.test.ts +73 -0
  346. package/src/client/recovery/session-dialog-mount.test.ts +54 -0
  347. package/src/client/run-store.ts +84 -21
  348. package/src/client/sync/AutosyncCard.tsx +25 -1
  349. package/src/client/sync/ChannelClearConfirmDialog.tsx +6 -3
  350. package/src/client/sync/ChannelConfigDialog.tsx +10 -10
  351. package/src/client/sync/DecryptPasswordCard.tsx +2 -2
  352. package/src/client/sync/SecurityOptionsCard.tsx +27 -17
  353. package/src/client/sync/SyncChannelEntryCard.tsx +14 -2
  354. package/src/client/sync/SyncConfirmView.tsx +46 -8
  355. package/src/client/sync/SyncLogList.tsx +1 -1
  356. package/src/client/sync/SyncPanel.tsx +31 -23
  357. package/src/client/sync/SyncRepositoryPicker.tsx +3 -5
  358. package/src/client/sync/SyncSectionPickerDialog.tsx +11 -2
  359. package/src/client/sync/SyncSettingsView.test.ts +20 -1
  360. package/src/client/sync/UnreadableSnapshotsBanner.tsx +1 -1
  361. package/src/client/sync/WorkDirCard.tsx +112 -0
  362. package/src/client/sync/history-model.test.ts +42 -9
  363. package/src/client/sync/history-model.ts +30 -15
  364. package/src/client/sync/sync-api.test.ts +57 -33
  365. package/src/client/sync/sync-api.ts +80 -6
  366. package/src/client/sync/sync-locales.ts +104 -3
  367. package/src/client/sync/sync-view-v2.test.ts +65 -13
  368. package/src/client/sync/sync-view.test.ts +107 -52
  369. package/src/client/sync/sync-view.ts +124 -41
  370. package/src/core/backup-plan.test.ts +28 -0
  371. package/src/core/backup-plan.ts +29 -17
  372. package/src/core/backup-verify.ts +24 -23
  373. package/src/core/backup.test.ts +76 -0
  374. package/src/core/backup.ts +55 -25
  375. package/src/core/boot-rescue.ts +4 -3
  376. package/src/core/boot-safety.ts +2 -1
  377. package/src/core/command-rollback.test.ts +536 -0
  378. package/src/core/command-rollback.ts +402 -0
  379. package/src/core/consult-source.ts +7 -6
  380. package/src/core/disk-usage.test.ts +257 -1
  381. package/src/core/disk-usage.ts +134 -14
  382. package/src/core/engine-locale.test.ts +593 -0
  383. package/src/core/engine-locale.ts +175 -0
  384. package/src/core/exporter.ts +3 -3
  385. package/src/core/fork-reconcile.test.ts +279 -0
  386. package/src/core/fork-reconcile.ts +234 -0
  387. package/src/core/journal.ts +6 -5
  388. package/src/core/local-plugin-host.ts +3 -2
  389. package/src/core/local-plugin-pack.ts +11 -6
  390. package/src/core/messages-core.ts +657 -0
  391. package/src/core/messages.ts +123 -17
  392. package/src/core/migration-consult.ts +21 -20
  393. package/src/core/migration-history.ts +19 -5
  394. package/src/core/model-tools.ts +28 -27
  395. package/src/core/phase3-host.ts +21 -5
  396. package/src/core/phase3-p2b-fingerprint.test.ts +22 -0
  397. package/src/core/plugin-cli.ts +18 -19
  398. package/src/core/reconcile.test.ts +117 -0
  399. package/src/core/reconcile.ts +85 -17
  400. package/src/core/recovery-orchestrator.ts +19 -18
  401. package/src/core/reinstall.ts +26 -21
  402. package/src/core/restore.ts +20 -17
  403. package/src/core/rollback-file-diff.test.ts +159 -0
  404. package/src/core/rollback-ignore-rules.ts +126 -0
  405. package/src/core/rollback-point.test.ts +2603 -0
  406. package/src/core/rollback-point.ts +3755 -0
  407. package/src/core/run-registry.ts +6 -5
  408. package/src/core/session-health.ts +15 -0
  409. package/src/core/session-meta.test.ts +117 -1
  410. package/src/core/session-meta.ts +149 -2
  411. package/src/core/session-title.test.ts +127 -0
  412. package/src/core/session-title.ts +189 -0
  413. package/src/core/startup-barrier.test.ts +69 -0
  414. package/src/core/startup-barrier.ts +27 -0
  415. package/src/core/transaction-coordinator.ts +4 -3
  416. package/src/core/types.ts +18 -1
  417. package/src/core/validator.ts +5 -4
  418. package/src/core/verify-recovery.ts +31 -30
  419. package/src/foreign/bundle.ts +3 -2
  420. package/src/foreign/continue.ts +2 -1
  421. package/src/foreign/crush.ts +2 -1
  422. package/src/foreign/file-budget.test.ts +2 -0
  423. package/src/foreign/gemini.ts +2 -1
  424. package/src/foreign/goose.ts +2 -1
  425. package/src/foreign/kilocode.ts +2 -1
  426. package/src/foreign/kimi.ts +2 -1
  427. package/src/foreign/messages.ts +44 -0
  428. package/src/foreign/mimocode.ts +2 -1
  429. package/src/foreign/openclaw.ts +2 -1
  430. package/src/foreign/opencode.ts +2 -1
  431. package/src/foreign/pi.ts +2 -1
  432. package/src/foreign/qoder.ts +2 -1
  433. package/src/foreign/qwen.ts +2 -1
  434. package/src/foreign/registry.ts +7 -6
  435. package/src/foreign/teleagent.ts +2 -1
  436. package/src/foreign/workbuddy.ts +2 -1
  437. package/src/foreign/zcode.ts +2 -1
  438. package/src/foreign/zed.ts +2 -1
  439. package/src/index.facade.test.ts +340 -1
  440. package/src/index.ts +813 -57
  441. package/src/market/git-file-writer.ts +20 -19
  442. package/src/market/github-repos.test.ts +23 -0
  443. package/src/market/github-repos.ts +37 -19
  444. package/src/market/index-parser.ts +31 -30
  445. package/src/market/messages-market.ts +325 -0
  446. package/src/market/my-repo.ts +32 -29
  447. package/src/market/prepare.ts +18 -15
  448. package/src/market/security.ts +20 -19
  449. package/src/market/types.ts +2 -1
  450. package/src/market/url.ts +2 -1
  451. package/src/migrations/index.ts +6 -5
  452. package/src/migrations/messages-migrations.ts +59 -0
  453. package/src/profiles/dsh-profile-launcher.ts +2 -1
  454. package/src/profiles/dsh-profile-runtime.ts +2 -1
  455. package/src/profiles/messages.ts +31 -0
  456. package/src/routes/backup.ts +18 -2
  457. package/src/routes/import.ts +3 -3
  458. package/src/routes/index.ts +1 -1
  459. package/src/routes/kit.ts +9 -3
  460. package/src/routes/market.ts +1 -1
  461. package/src/routes/messages.ts +104 -0
  462. package/src/routes/profiles.ts +12 -6
  463. package/src/routes/recovery.test.ts +405 -1
  464. package/src/routes/recovery.ts +377 -109
  465. package/src/routes/snapshots.ts +51 -4
  466. package/src/routes/sync.ts +67 -16
  467. package/src/schema/config.ts +53 -20
  468. package/src/schema/index.ts +7 -5
  469. package/src/schema/manifest.ts +19 -18
  470. package/src/schema/messages.ts +120 -0
  471. package/src/schema/registry.test.ts +19 -5
  472. package/src/schema/section-registry.ts +64 -39
  473. package/src/schema/versions.ts +6 -5
  474. package/src/security/encryption.ts +16 -15
  475. package/src/security/integrity.ts +8 -7
  476. package/src/security/messages.ts +87 -0
  477. package/src/security/redaction.test.ts +17 -1
  478. package/src/security/redaction.ts +13 -0
  479. package/src/security/secret-scanner.ts +19 -12
  480. package/src/security/security.test.ts +47 -0
  481. package/src/security/vault.ts +2 -1
  482. package/src/security/zip-security.ts +4 -3
  483. package/src/sync/ancestor.ts +2 -1
  484. package/src/sync/autosync-config.ts +21 -0
  485. package/src/sync/autosync-scheduler.test.ts +63 -7
  486. package/src/sync/autosync-scheduler.ts +118 -78
  487. package/src/sync/backup-files.ts +3 -2
  488. package/src/sync/backup-scheduler.ts +6 -5
  489. package/src/sync/blob-store.ts +2 -1
  490. package/src/sync/git/git-transport.test.ts +170 -8
  491. package/src/sync/git/git-transport.ts +176 -2
  492. package/src/sync/github-auth.ts +10 -9
  493. package/src/sync/layout.ts +13 -12
  494. package/src/sync/messages.ts +153 -0
  495. package/src/sync/review-queue.ts +6 -5
  496. package/src/sync/s3/messages.ts +16 -0
  497. package/src/sync/s3/s3-providers.ts +9 -6
  498. package/src/sync/s3/s3-transport.ts +1 -1
  499. package/src/sync/s3/sigv4.ts +3 -1
  500. package/src/sync/snapshot-crypto.ts +6 -5
  501. package/src/sync/snapshot-json.ts +3 -2
  502. package/src/sync/sync-engine-download-tombstone.test.ts +1 -1
  503. package/src/sync/sync-engine.test.ts +226 -8
  504. package/src/sync/sync-engine.ts +103 -41
  505. package/src/sync/sync-history.ts +7 -6
  506. package/src/sync/sync-selection.test.ts +12 -2
  507. package/src/sync/sync-selection.ts +18 -5
  508. package/src/sync/webdav/webdav-transport.ts +4 -1
  509. package/src/ui/artifact-view.ts +4 -3
  510. package/src/ui/commands.test.ts +1 -0
  511. package/src/ui/commands.ts +11 -1
  512. package/src/ui/disk-usage-view.test.ts +113 -6
  513. package/src/ui/disk-usage-view.ts +63 -7
  514. package/src/ui/errors.ts +4 -3
  515. package/src/ui/export-flow.ts +24 -20
  516. package/src/ui/foreign-view.test.ts +43 -0
  517. package/src/ui/foreign-view.ts +30 -3
  518. package/src/ui/i18n.ts +125 -0
  519. package/src/ui/import-wizard.ts +18 -10
  520. package/src/ui/progress.ts +2 -2
  521. package/src/ui/report.ts +7 -6
  522. package/src/ui/rollback-view.test.ts +819 -0
  523. package/src/ui/rollback-view.ts +1339 -0
  524. package/src/ui/selection-model.test.ts +33 -1
  525. package/src/ui/selection-model.ts +53 -0
  526. package/src/ui/session-inventory-view.test.ts +109 -0
  527. package/src/ui/session-inventory-view.ts +138 -3
  528. package/src/ui/session-issue-labels.test.ts +51 -0
  529. package/src/ui/session-issue-labels.ts +70 -0
  530. package/src/ui/sync-settings-view.test.ts +36 -0
  531. package/src/ui/sync-settings-view.ts +49 -0
  532. package/src/ui/types.ts +10 -4
  533. package/src/utils/atomic-write.ts +6 -5
  534. package/src/utils/bundle-scan.test.ts +2 -2
  535. package/src/utils/bundle-scan.ts +12 -1
  536. package/src/utils/env-lock.ts +83 -49
  537. package/src/utils/json.ts +7 -5
  538. package/src/utils/messages-utils.ts +232 -0
  539. package/src/utils/proxy.test.ts +25 -0
  540. package/src/utils/proxy.ts +19 -5
  541. package/src/utils/recursive-walk.test.ts +43 -0
  542. package/src/utils/recursive-walk.ts +75 -1
  543. package/src/utils/session-log-repair.ts +10 -9
  544. package/src/utils/zip.ts +27 -26
  545. package/lib/core/checkpoint.d.ts +0 -362
  546. package/lib/core/checkpoint.js +0 -1309
  547. package/src/core/checkpoint.test.ts +0 -665
  548. package/src/core/checkpoint.ts +0 -1614
@@ -28,6 +28,7 @@
28
28
  * (实测 `verifyChecksums` 只调用 `map.get()`,不会整体物化 Map)。
29
29
  * 对 MB 级的 DSH 配置备份完全可接受。
30
30
  */
31
+ import { engineMsg } from './messages-core.ts';
31
32
  import fs from 'node:fs/promises';
32
33
  import { verifyChecksums } from '../utils/hashing.ts';
33
34
  import { isENOENT } from '../utils/guards.ts';
@@ -146,18 +147,18 @@ export async function verifyBackupZip(zipPath: string): Promise<BackupVerifyResu
146
147
  try {
147
148
  const stat = await fs.stat(zipPath);
148
149
  if (!stat.isFile()) {
149
- errors.push(`不是普通文件(目录或特殊文件)/ not a regular file: ${zipPath}`);
150
+ errors.push(engineMsg('core.backupVerify.notRegularFile', { zip: zipPath }));
150
151
  return makeResult(zipPath, 'MISSING', errors, warnings);
151
152
  }
152
153
  sizeBytes = stat.size;
153
154
  buf = await fs.readFile(zipPath);
154
155
  } catch (err) {
155
156
  if (isENOENT(err)) {
156
- errors.push(`备份文件不存在 / backup file not found: ${zipPath}`);
157
+ errors.push(engineMsg('core.backupVerify.fileNotFound', { zip: zipPath }));
157
158
  return makeResult(zipPath, 'MISSING', errors, warnings);
158
159
  }
159
160
  // 文件在但读不动(EACCES / EBUSY / IO 错):这是自检自身的失败,不是「备份坏了」
160
- errors.push(`备份文件不可读 / backup file unreadable: ${msgOfError(err)}`);
161
+ errors.push(engineMsg('core.backupVerify.fileUnreadable', { reason: msgOfError(err) }));
161
162
  return makeResult(zipPath, 'VERIFY_ERROR', errors, warnings);
162
163
  }
163
164
 
@@ -170,7 +171,7 @@ export async function verifyBackupZip(zipPath: string): Promise<BackupVerifyResu
170
171
  /* ---------------- 2. 打开为 ZIP ---------------- */
171
172
  // 整体加密容器不是 ZIP(先识别,给出比「ZIP 损坏」准确得多的结论)
172
173
  if (isArchiveBlob(buf)) {
173
- errors.push('整体加密备份容器(DCA1)需先用密码解密为 ZIP 才能自检 / encrypted container must be decrypted first');
174
+ errors.push(engineMsg('core.backupVerify.encryptedContainer'));
174
175
  return withSize('UNSUPPORTED');
175
176
  }
176
177
 
@@ -178,7 +179,7 @@ export async function verifyBackupZip(zipPath: string): Promise<BackupVerifyResu
178
179
  try {
179
180
  archive = createHardenedZipParser()(buf);
180
181
  } catch (err) {
181
- errors.push(`ZIP 无法打开 / cannot open as ZIP: ${msgOfError(err)}`);
182
+ errors.push(engineMsg('core.backupVerify.cannotOpenZip', { reason: msgOfError(err) }));
182
183
  return withSize('CORRUPT');
183
184
  }
184
185
  const metas = archive.entries();
@@ -187,13 +188,13 @@ export async function verifyBackupZip(zipPath: string): Promise<BackupVerifyResu
187
188
  file: zipPath, verdict, sizeBytes, entryCount, errors, warnings,
188
189
  });
189
190
  if (entryCount === 0) {
190
- errors.push('空 ZIP(0 条目)/ empty archive');
191
+ errors.push(engineMsg('core.backupVerify.emptyArchive'));
191
192
  return withEntries('CORRUPT');
192
193
  }
193
194
 
194
195
  /* ---------------- 3. manifest.json ---------------- */
195
196
  if (!archive.has(MANIFEST_FILE)) {
196
- errors.push(`缺少 ${MANIFEST_FILE}(不是本插件导出的备份)/ ${MANIFEST_FILE} missing`);
197
+ errors.push(engineMsg('core.backupVerify.missingManifest', { file: MANIFEST_FILE }));
197
198
  return withEntries('CORRUPT');
198
199
  }
199
200
  let manifestRaw: string;
@@ -201,14 +202,14 @@ export async function verifyBackupZip(zipPath: string): Promise<BackupVerifyResu
201
202
  manifestRaw = archive.readEntryText(MANIFEST_FILE);
202
203
  } catch (err) {
203
204
  // 条目本身读不出来(CRC 失败/解压失败)→ 归档损坏,而非 manifest 语义非法
204
- errors.push(`${MANIFEST_FILE} 无法读取(归档损坏)/ unreadable entry: ${msgOfError(err)}`);
205
+ errors.push(engineMsg('core.backupVerify.manifestUnreadable', { file: MANIFEST_FILE, reason: msgOfError(err) }));
205
206
  return withEntries('CORRUPT');
206
207
  }
207
208
  let manifest: Manifest;
208
209
  try {
209
210
  manifest = parseManifest(manifestRaw);
210
211
  } catch (err) {
211
- errors.push(`${MANIFEST_FILE} 非法 / invalid manifest: ${msgOfError(err)}`);
212
+ errors.push(engineMsg('core.backupVerify.invalidManifest', { file: MANIFEST_FILE, reason: msgOfError(err) }));
212
213
  return withEntries('CORRUPT');
213
214
  }
214
215
  const sections = Object.entries(manifest.sections ?? {})
@@ -220,13 +221,13 @@ export async function verifyBackupZip(zipPath: string): Promise<BackupVerifyResu
220
221
 
221
222
  if (!isSupported(manifest.schemaVersion)) {
222
223
  // 版本超范围是「本插件读不了」,不是「备份坏了」——明确区分,避免误导用户重导
223
- errors.push(`schema 版本不受支持 / unsupported schema version: ${describeVersion(manifest.schemaVersion)}`);
224
+ errors.push(engineMsg('core.backupVerify.unsupportedSchema', { version: describeVersion(manifest.schemaVersion) }));
224
225
  return withManifest('UNSUPPORTED');
225
226
  }
226
227
 
227
228
  /* ---------------- 4. integrity/checksums.json ---------------- */
228
229
  if (!archive.has(CHECKSUMS_FILE)) {
229
- errors.push(`缺少 ${CHECKSUMS_FILE}(无法做完整性校验)/ ${CHECKSUMS_FILE} missing`);
230
+ errors.push(engineMsg('core.backupVerify.missingChecksums', { file: CHECKSUMS_FILE }));
230
231
  return withManifest('CORRUPT');
231
232
  }
232
233
  let table: Record<string, string>;
@@ -234,7 +235,7 @@ export async function verifyBackupZip(zipPath: string): Promise<BackupVerifyResu
234
235
  table = parseChecksumsTable(archive.readEntryText(CHECKSUMS_FILE));
235
236
  } catch (err) {
236
237
  // readEntryText 的 CRC/解压失败与「表结构非法」都归 CORRUPT(两者都说明备份不可信)
237
- errors.push(`${CHECKSUMS_FILE} 非法或不可读 / invalid or unreadable checksums table: ${msgOfError(err)}`);
238
+ errors.push(engineMsg('core.backupVerify.invalidChecksums', { file: CHECKSUMS_FILE, reason: msgOfError(err) }));
238
239
  return withManifest('CORRUPT');
239
240
  }
240
241
 
@@ -243,7 +244,7 @@ export async function verifyBackupZip(zipPath: string): Promise<BackupVerifyResu
243
244
  const unreadableNames = new Set<string>();
244
245
  const unreadable: string[] = [];
245
246
  const lazyEntries = new LazyEntryMap(archive, unreadableNames, (name, reason) => {
246
- unreadable.push(`${name}(${reason})`);
247
+ unreadable.push(engineMsg('core.backupVerify.unreadableEntry', { name, reason }));
247
248
  });
248
249
  const tableKeys = Object.keys(table);
249
250
  const check = verifyChecksums(lazyEntries, table);
@@ -254,10 +255,10 @@ export async function verifyBackupZip(zipPath: string): Promise<BackupVerifyResu
254
255
  // 4b. 结构提示:未纳入校验表的条目(导出器只把 manifest 与校验表自身排除在外)
255
256
  if (tableKeys.length === 0) {
256
257
  if (entryCount > NON_DATA_ENTRIES.size) {
257
- errors.push('校验表为空但归档含数据条目 / empty checksums table with data entries');
258
+ errors.push(engineMsg('core.backupVerify.emptyTableWithData'));
258
259
  return withManifest('CORRUPT');
259
260
  }
260
- warnings.push('校验表为空(归档仅含 manifest 与校验表自身)/ empty checksums table');
261
+ warnings.push(engineMsg('core.backupVerify.emptyTable'));
261
262
  }
262
263
  const covered = new Set(tableKeys);
263
264
  const uncovered = metas
@@ -265,7 +266,7 @@ export async function verifyBackupZip(zipPath: string): Promise<BackupVerifyResu
265
266
  .map((m) => m.name);
266
267
  if (uncovered.length > 0) {
267
268
  const shown = uncovered.slice(0, 5).join(', ');
268
- warnings.push(`含未纳入校验表的条目 ${uncovered.length} 个 / entries not covered by checksums: ${shown}${uncovered.length > 5 ? ', …' : ''}`);
269
+ warnings.push(engineMsg('core.backupVerify.uncoveredEntries', { count: uncovered.length, shown, suffix: uncovered.length > 5 ? ', …' : '' }));
269
270
  }
270
271
 
271
272
  // 4c. 结构提示:manifest 声明包含的分区在归档内应有对应条目
@@ -274,30 +275,30 @@ export async function verifyBackupZip(zipPath: string): Promise<BackupVerifyResu
274
275
  const jsonPath = SECTION_JSON_PATHS[id as SectionId];
275
276
  if (prefix !== undefined) {
276
277
  if (!metas.some((m) => m.name.startsWith(prefix) && m.name !== prefix)) {
277
- warnings.push(`manifest 声明分区 ${id} 但归档内无对应文件 / declared section has no entries: ${id}`);
278
+ warnings.push(engineMsg('core.backupVerify.declaredSectionNoEntries', { id }));
278
279
  }
279
280
  } else if (jsonPath !== undefined && !archive.has(jsonPath)) {
280
- warnings.push(`manifest 声明分区 ${id} 但缺少 ${jsonPath} / declared section payload missing: ${id}`);
281
+ warnings.push(engineMsg('core.backupVerify.declaredSectionPayloadMissing', { id, path: jsonPath }));
281
282
  }
282
283
  }
283
284
  if (manifest.security?.encrypted === true && !metas.some((m) => SECRETS_ENC_RE.test(m.name))) {
284
- warnings.push('manifest 标记加密但归档内无凭据密文条目 / encrypted flag without secrets entry');
285
+ warnings.push(engineMsg('core.backupVerify.encryptedFlagNoSecrets'));
285
286
  }
286
287
 
287
288
  /* ---------------- 裁决 ---------------- */
288
289
  if (missing.length > 0) {
289
- errors.push(`校验表内条目缺失(${missing.length})/ missing entries: ${missing.join(', ')}`);
290
+ errors.push(engineMsg('core.backupVerify.missingEntries', { count: missing.length, list: missing.join(', ') }));
290
291
  }
291
292
  if (mismatched.length > 0) {
292
- errors.push(`条目 SHA-256 不符(${mismatched.length})/ hash mismatch: ${mismatched.join(', ')}`);
293
+ errors.push(engineMsg('core.backupVerify.hashMismatch', { count: mismatched.length, list: mismatched.join(', ') }));
293
294
  }
294
295
  if (unreadable.length > 0) {
295
- errors.push(`条目无法读取(${unreadable.length})/ unreadable entries: ${unreadable.join(', ')}`);
296
+ errors.push(engineMsg('core.backupVerify.unreadableEntries', { count: unreadable.length, list: unreadable.join(', ') }));
296
297
  }
297
298
  return withManifest(missing.length > 0 || mismatched.length > 0 || unreadable.length > 0 ? 'CORRUPT' : 'OK');
298
299
  } catch (err) {
299
300
  // 5. 自检自身异常:如实报 VERIFY_ERROR,绝不降级成 OK
300
- errors.push(`自检过程异常 / verification error: ${msgOfError(err)}`);
301
+ errors.push(engineMsg('core.backupVerify.verificationError', { reason: msgOfError(err) }));
301
302
  return withSize('VERIFY_ERROR');
302
303
  }
303
304
  }
@@ -344,3 +344,79 @@ test('F-4: normalizeDshVersion 归一(去空白 / unknown 丢弃 / 非字符
344
344
  assert.equal(normalizeDshVersion(undefined), undefined);
345
345
  assert.equal(normalizeDshVersion(42), undefined);
346
346
  });
347
+
348
+ /* ---------------- 宿主文件「存在但读不出来」不得记成 existed:false(数据安全) ---------------- */
349
+
350
+ test('备份:宿主文件存在但读不出来 → existed=true + unreadable=true(绝不写 existed:false)', async (t) => {
351
+ const dir = await tmpDir(t, 'unreadable');
352
+ const { ctx } = countingCtx();
353
+ await seedHostFiles(ctx);
354
+ // 让 settings.yaml 这一次读不出来(占用 / 权限 / IO 错的等价物)
355
+ const fsFacade = ctx.fs as { readFile: (rel: string) => Promise<Uint8Array> };
356
+ const original = fsFacade.readFile.bind(ctx.fs);
357
+ fsFacade.readFile = async (rel: string) => {
358
+ if (rel === 'settings.yaml') throw new Error('EBUSY: resource busy or locked');
359
+ return original(rel);
360
+ };
361
+ const store = new FileSnapshotStore({ dir });
362
+ const snapshot = await createSnapshot({ ctx, plan: emptyPlan(), sourceZip: 'unit.zip', store, adapters: [] });
363
+
364
+ const settings = (snapshot.hostFileBackups ?? []).find((b) => b.relPath === 'settings.yaml');
365
+ assert.ok(settings, 'settings.yaml 必须仍被登记(绝不因读失败而消失)');
366
+ assert.equal(settings.existed, true, '存在但读不到 → existed 必须为 true(写成 false 会在恢复时删掉用户真实文件)');
367
+ assert.equal(settings.unreadable, true, '必须显式标注 unreadable');
368
+ assert.equal(settings.blobPath, '', '读不到 → 没有 blob');
369
+ const patch = (snapshot.hostFileBackups ?? []).find((b) => b.relPath === 'cordis.patch.yml');
370
+ assert.equal(patch?.existed, true, '其余宿主文件照常读');
371
+ assert.equal(patch?.unreadable, undefined, '读得到的条目不得被标 unreadable');
372
+ assert.equal((await verifySnapshot(dir, snapshot.id)).ok, true, 'unreadable 条目不得破坏 durable+verified');
373
+ });
374
+
375
+ /* ---------------- 保留清理 fail-closed:引用集合读不出来时本轮绝不删(数据安全) ---------------- */
376
+
377
+ test('保留清理:recovery 引用集合读不出来 → 本轮放弃清理,绝不按空集合删(F3 不可回退)', async (t) => {
378
+ const dir = await tmpDir(t, 'prune-ref');
379
+ const { ctx } = countingCtx();
380
+ await seedHostFiles(ctx);
381
+ let scans = 0;
382
+ const warned: string[] = [];
383
+ const store = new FileSnapshotStore({
384
+ dir,
385
+ // 引用扫描一次瞬时失败(EACCES / IO 错)—— 此前会被 .catch 成空集合,随即按「无人引用」继续删
386
+ referencedSnapshotIds: async () => { scans += 1; throw new Error('EACCES: permission denied'); },
387
+ retentionPolicy: () => ({ keepLast: 1, keepMonthly: 0, keepYearly: 0 }),
388
+ onWarn: (message) => { warned.push(message) },
389
+ });
390
+
391
+ const first = await createSnapshot({ ctx, plan: emptyPlan(), sourceZip: 'unit.zip', store, adapters: [] });
392
+ const second = await createSnapshot({ ctx, plan: emptyPlan(), sourceZip: 'unit.zip', store, adapters: [] });
393
+
394
+ assert.ok(scans >= 1, 'prune 必须真的问过 recovery 引用集合(否则本用例不成立)');
395
+ const remaining = (await fs.readdir(dir)).sort();
396
+ assert.deepEqual(remaining, [first.id, second.id].sort(), '读不到引用集合时本轮必须放弃清理 —— 两个快照都得留着');
397
+ assert.ok(warned.some((m) => m.includes('放弃')), '放弃必须留痕(绝不静默):' + JSON.stringify(warned));
398
+ });
399
+
400
+ /* ---------------- 保留清理:目录名 ≠ 内部 id 时跳过并留痕(数据安全) ---------------- */
401
+
402
+ test('保留清理:快照目录名与内部 id 不一致时跳过并留痕(绝不按内部 id 删另一个目录)', async (t) => {
403
+ const dir = await tmpDir(t, 'prune-name');
404
+ const { ctx } = countingCtx();
405
+ await seedHostFiles(ctx);
406
+ const warned: string[] = [];
407
+ const store = new FileSnapshotStore({
408
+ dir,
409
+ retentionPolicy: () => ({ keepLast: 1, keepMonthly: 0, keepYearly: 0 }),
410
+ onWarn: (message) => { warned.push(message) },
411
+ });
412
+ const first = await createSnapshot({ ctx, plan: emptyPlan(), sourceZip: 'unit.zip', store, adapters: [] });
413
+ // 手工制造「目录名 ≠ 内部 id」(改名目录 / 半截拷贝残留的真机形态)
414
+ const renamed = path.join(dir, 'zzz-renamed-dir');
415
+ await fs.rename(path.join(dir, first.id), renamed);
416
+ const second = await createSnapshot({ ctx, plan: emptyPlan(), sourceZip: 'unit.zip', store, adapters: [] });
417
+
418
+ const remaining = (await fs.readdir(dir)).sort();
419
+ assert.ok(remaining.includes('zzz-renamed-dir'), '目录名与内部 id 不一致的目录绝不能被当成候选删掉');
420
+ assert.ok(remaining.includes(second.id), '新建的快照当然还在');
421
+ assert.ok(warned.some((m) => m.includes('不一致')), '跳过必须留痕(绝不静默):' + JSON.stringify(warned));
422
+ });
@@ -5,6 +5,7 @@
5
5
  * 快照不保存 credential 值(DSH 永不回读值)——credential 条目只记 existed 标志,
6
6
  * 回滚时 existed=true 的条目如实标记「值需人工补录」。
7
7
  */
8
+ import { engineMsg } from './messages-core.ts';
8
9
  import fs from 'node:fs/promises';
9
10
  import path from 'node:path';
10
11
  import crypto from 'node:crypto';
@@ -103,8 +104,11 @@ async function backupHostFiles(
103
104
  try {
104
105
  existed = await ctx.fs.exists(relPath);
105
106
  } catch (err) {
107
+ // 探测自身失败 = 存在性判不出来:按「存在但不可读」登记。**绝不写 existed:false** ——
108
+ // 那会被恢复侧读成「当时确实不存在」,进而删掉用户真实的文件。
106
109
  ctx.log.warn(`快照探测宿主文件失败 ${relPath}: ${err instanceof Error ? err.message : String(err)}`);
107
- existed = false;
110
+ backups.push({ relPath, blobPath: '', existed: true, unreadable: true });
111
+ continue;
108
112
  }
109
113
  if (!existed) {
110
114
  backups.push({ relPath, blobPath: '', existed: false });
@@ -115,8 +119,10 @@ async function backupHostFiles(
115
119
  blobs.set(blobPath, await ctx.fs.readFile(relPath));
116
120
  backups.push({ relPath, blobPath, existed: true });
117
121
  } catch (err) {
122
+ // 文件确实存在、只是这次读不出来(占用/权限/IO):登记 existed=true + unreadable,
123
+ // 恢复侧据此只跳过并告警,绝不进入「删除」分支(见 HostFileBackup.unreadable)。
118
124
  ctx.log.warn(`快照读取宿主文件失败 ${relPath}: ${err instanceof Error ? err.message : String(err)}`);
119
- backups.push({ relPath, blobPath: '', existed: false });
125
+ backups.push({ relPath, blobPath: '', existed: true, unreadable: true });
120
126
  }
121
127
  }
122
128
  return backups;
@@ -142,7 +148,7 @@ const FILE_BASES: Partial<Record<SectionId, string>> = {
142
148
  // 其它分区语义处理」同类缺陷)。注册表新增文件类分区时若忘了在这里补,进程启动即失败。
143
149
  for (const id of SECTION_IDS) {
144
150
  if (isFileSection(id) && FILE_BASES[id] === undefined) {
145
- throw new Error(`分区 ${id} 是文件类分区,但 core/backup.ts 的 FILE_BASES 未给基准目录(拒绝静默退回 homeDir 根)`);
151
+ throw new Error(engineMsg('core.backup.fileSectionBaseMissing', { section: id }));
146
152
  }
147
153
  }
148
154
 
@@ -316,7 +322,7 @@ async function engineSnapshotEntry(ctx: HostContext, target: SnapshotTarget): Pr
316
322
  // secrets 已注册但**没有 adapter、也没有 ZIP 内分区 JSON**(凭据值走独立加密容器 secrets.enc)。
317
323
  // 任何计划项都不应以它为快照目标 —— 显式报错,而不是落到 default 分支。
318
324
  throw new Error(
319
- 'secrets 分区没有 adapter / 无分区载荷,不应作为快照目标(凭据值走独立加密容器,绝不进快照)',
325
+ engineMsg('core.backup.secretsSectionNotSnapshotTarget'),
320
326
  );
321
327
  default: {
322
328
  // 编译期穷举断言:SectionId 的每一项都已被上面的 case 处理 → 此处 target.adapter 收窄为 never。
@@ -326,8 +332,7 @@ async function engineSnapshotEntry(ctx: HostContext, target: SnapshotTarget): Pr
326
332
  // 旧实现在这里静默返回 settingsNamespace 条目(连 existed 都没有)→ 回滚把该目标当「原本不存在」
327
333
  // 而不做任何补偿:该分区的写入永远撤不掉,报告却仍说「已快照、可回滚」(审计 core-flow#F-10)。
328
334
  throw new Error(
329
- `快照不支持分区「${String(unhandled)}」:它未在 schema/section-registry.ts 注册(或缺少快照实现);`
330
- + '拒绝按其它分区语义静默记录(那会让该分区的写入无法回滚)',
335
+ engineMsg('core.backup.sectionUnsupported', { section: String(unhandled) }),
331
336
  );
332
337
  }
333
338
  }
@@ -534,6 +539,11 @@ export interface FileSnapshotStoreOptions {
534
539
  * 未注入 → `fallbackPruneSelector`(只按 keepLast,等价既有 FIFO;分层字段被忽略 = 保守少删)。
535
540
  */
536
541
  pruneSelector?: PruneSelector;
542
+ /**
543
+ * 诊断回调(可选):保留清理**被放弃**或失败时的可见线索。
544
+ * 缺省 = 不记录(不改变行为);宿主接 `host.log.warn` —— 「这轮为什么没清理」必须能查。
545
+ */
546
+ onWarn?: (message: string) => void;
537
547
  }
538
548
 
539
549
  /** 文件快照存储:<dir>/<id>/snapshot.json + <dir>/<id>/blobs/* */
@@ -554,7 +564,7 @@ export class FileSnapshotStore implements SnapshotStore {
554
564
  */
555
565
  private async writeBlob(dir: string, blobPath: string, data: Uint8Array, hashes: Record<string, string>): Promise<void> {
556
566
  const target = path.join(dir, blobPath);
557
- if (!target.startsWith(dir)) throw new Error(`快照 blob 路径越界: ${blobPath}`);
567
+ if (!target.startsWith(dir)) throw new Error(engineMsg('core.backup.blobPathEscaped', { path: blobPath }));
558
568
  await fs.mkdir(path.dirname(target), { recursive: true });
559
569
  await atomicWriteFile(target, data);
560
570
  hashes[blobPath] = sha256Hex(data);
@@ -579,7 +589,7 @@ export class FileSnapshotStore implements SnapshotStore {
579
589
  // 4) 验证(从磁盘重读,不信任内存对象)
580
590
  const verified = await verifySnapshot(this.options.dir, snapshot.id);
581
591
  if (!verified.ok) {
582
- throw new Error(`快照 ${snapshot.id} 验证失败: ${verified.reason}`);
592
+ throw new Error(engineMsg('core.backup.snapshotVerifyFailed', { id: snapshot.id, reason: verified.reason ?? '' }));
583
593
  }
584
594
  // 5) 原子发布 READY(重写 snapshot.json readiness='READY';metadataHash 稳定不受影响)
585
595
  const ready = { ...snapshot, readiness: 'READY' as const };
@@ -588,7 +598,9 @@ export class FileSnapshotStore implements SnapshotStore {
588
598
  try {
589
599
  await this.prune();
590
600
  } catch (err) {
591
- // 快照已 READY + verified;prune 失败(EBUSY/EPERM)不应把刚成功的快照判为 unusable
601
+ // 快照已 READY + verified;prune 失败(EBUSY/EPERM)不应把刚成功的快照判为 unusable ——
602
+ // 但必须留痕:保留清理长期失败会让快照无限堆积,用户与日志都看不到线索。
603
+ this.options.onWarn?.(engineMsg('core.backup.pruneFailed', { reason: err instanceof Error ? err.message : String(err) }));
592
604
  }
593
605
  return snapshot.id;
594
606
  }
@@ -632,7 +644,18 @@ export class FileSnapshotStore implements SnapshotStore {
632
644
  private async prune(): Promise<void> {
633
645
  const dir = this.options.dir;
634
646
  const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => []);
635
- const referenced = await this.options.referencedSnapshotIds?.().catch(() => new Set<string>()) ?? new Set<string>();
647
+ // F3:被未收敛 journal 引用的快照**绝不自动清理**。判据是「读不到 ≠ 没有引用」——
648
+ // 引用提供者抛错时**放弃本轮清理**,绝不按空集合继续删:一次瞬时 IO 错就可能删掉
649
+ // 事故恢复唯一的回滚点(那是 SAFE MODE 的唯一证据)。此前这里是 `.catch(() => new Set())`。
650
+ let referenced = new Set<string>();
651
+ if (this.options.referencedSnapshotIds !== undefined) {
652
+ try {
653
+ referenced = await this.options.referencedSnapshotIds();
654
+ } catch (err) {
655
+ this.options.onWarn?.(engineMsg('core.backup.pruneAbandoned', { reason: err instanceof Error ? err.message : String(err) }));
656
+ return;
657
+ }
658
+ }
636
659
  // m-retention:策略读取失败 → 回退缺省(绝不因策略提供者异常而误删/崩溃)
637
660
  const policy = await Promise.resolve()
638
661
  .then(() => this.options.retentionPolicy?.() ?? defaultRetentionPolicyLike())
@@ -644,9 +667,16 @@ export class FileSnapshotStore implements SnapshotStore {
644
667
  try {
645
668
  const parsed = parseJsonSafe(await fs.readFile(path.join(dir, entry.name, 'snapshot.json'), 'utf8')) as Snapshot;
646
669
  if (typeof parsed.id !== 'string' || parsed.id === '' || typeof parsed.createdAt !== 'string') continue;
670
+ // 删除目标只能是**目录名**(下面 path.join(dir, id) 就是按它删的)。目录名与内部 id 不一致时
671
+ // **绝不猜**:pinned/被引用的判断是对着这份 snapshot.json 做的,删除却会打在另一个目录上 ——
672
+ // 跳过并留痕(手工改名/半截拷贝残留会出现这种形态)。
673
+ if (parsed.id !== entry.name) {
674
+ this.options.onWarn?.(engineMsg('core.backup.dirNameIdMismatch', { dir: entry.name, id: parsed.id }));
675
+ continue;
676
+ }
647
677
  if (parsed.pinned === true) continue; // 置顶快照豁免自动清理
648
- if (referenced.has(parsed.id)) continue; // Phase 4 F3:recovery 引用豁免
649
- metas.push({ id: parsed.id, createdAt: parsed.createdAt });
678
+ if (referenced.has(entry.name)) continue; // Phase 4 F3:recovery 引用豁免
679
+ metas.push({ id: entry.name, createdAt: parsed.createdAt });
650
680
  } catch {
651
681
  // 损坏 / 非快照目录:跳过(与 listSnapshots 语义一致)
652
682
  }
@@ -672,7 +702,7 @@ export class FileSnapshotStore implements SnapshotStore {
672
702
  async readBlob(id: string, blobPath: string): Promise<Uint8Array> {
673
703
  const target = path.join(this.snapshotDir(id), blobPath);
674
704
  const dir = this.options.dir;
675
- if (!target.startsWith(dir)) throw new Error(`快照 blob 路径越界: ${blobPath}`);
705
+ if (!target.startsWith(dir)) throw new Error(engineMsg('core.backup.blobPathEscaped', { path: blobPath }));
676
706
  return fs.readFile(target);
677
707
  }
678
708
 
@@ -712,51 +742,51 @@ export interface SnapshotVerifyResult {
712
742
  export async function verifySnapshot(snapshotsDir: string, id: string): Promise<SnapshotVerifyResult> {
713
743
  const dir = path.join(snapshotsDir, id);
714
744
  if (!dir.startsWith(path.resolve(snapshotsDir) + path.sep)) {
715
- return { ok: false, reason: '快照路径越界' };
745
+ return { ok: false, reason: engineMsg('core.backup.snapshotPathEscaped') };
716
746
  }
717
747
  let raw: string;
718
748
  try {
719
749
  raw = await fs.readFile(path.join(dir, 'snapshot.json'), 'utf8');
720
750
  } catch {
721
- return { ok: false, reason: 'snapshot.json 缺失' };
751
+ return { ok: false, reason: engineMsg('core.backup.snapshotJsonMissing') };
722
752
  }
723
753
  const snapshot = parseJsonSafe(raw) as Snapshot;
724
754
  if (snapshot === null || typeof snapshot !== 'object' || typeof snapshot.id !== 'string' || snapshot.id === '') {
725
- return { ok: false, reason: 'snapshot.json 非法' };
755
+ return { ok: false, reason: engineMsg('core.backup.snapshotJsonInvalid') };
726
756
  }
727
757
  if (snapshot.id !== id) {
728
- return { ok: false, reason: 'snapshot.id 与目录不匹配' };
758
+ return { ok: false, reason: engineMsg('core.backup.snapshotIdDirMismatch') };
729
759
  }
730
760
  let manifestRaw: string;
731
761
  try {
732
762
  manifestRaw = await fs.readFile(path.join(dir, 'manifest.json'), 'utf8');
733
763
  } catch {
734
- return { ok: false, reason: 'manifest.json 缺失' };
764
+ return { ok: false, reason: engineMsg('core.backup.manifestMissing') };
735
765
  }
736
766
  const manifest = parseJsonSafe(manifestRaw) as SnapshotManifest;
737
767
  if (manifest === null || typeof manifest !== 'object' || manifest.snapshotId !== id) {
738
- return { ok: false, reason: 'manifest 非法或 snapshotId 不匹配' };
768
+ return { ok: false, reason: engineMsg('core.backup.manifestInvalid') };
739
769
  }
740
770
  // entryCount 匹配(manifest 声明条目数与实际一致,防计数篡改)
741
771
  if (typeof manifest.entryCount !== 'number' || manifest.entryCount !== snapshot.entries.length) {
742
- return { ok: false, reason: 'manifest.entryCount 与 snapshot.entries 不一致(篡改)' };
772
+ return { ok: false, reason: engineMsg('core.backup.entryCountMismatch') };
743
773
  }
744
774
  // metadataHash 匹配(破坏性内容)
745
775
  if (computeMetadataHash(snapshot) !== manifest.metadataHash) {
746
- return { ok: false, reason: 'metadataHash 不匹配(破坏性内容被篡改)' };
776
+ return { ok: false, reason: engineMsg('core.backup.metadataHashMismatch') };
747
777
  }
748
778
  // blob hashes 匹配 + 存在
749
779
  for (const [blobPath, expectedHash] of Object.entries(manifest.blobHashes)) {
750
780
  const target = path.join(dir, blobPath);
751
- if (!target.startsWith(dir)) return { ok: false, reason: `blob 路径越界: ${blobPath}` };
781
+ if (!target.startsWith(dir)) return { ok: false, reason: engineMsg('core.backup.blobPathEscaped', { path: blobPath }) };
752
782
  let data: Uint8Array;
753
783
  try {
754
784
  data = await fs.readFile(target);
755
785
  } catch {
756
- return { ok: false, reason: `blob 缺失: ${blobPath}` };
786
+ return { ok: false, reason: engineMsg('core.backup.blobMissing', { path: blobPath }) };
757
787
  }
758
788
  if (sha256Hex(data) !== expectedHash) {
759
- return { ok: false, reason: `blob hash 不匹配: ${blobPath}` };
789
+ return { ok: false, reason: engineMsg('core.backup.blobHashMismatch', { path: blobPath }) };
760
790
  }
761
791
  }
762
792
  // Review C P2:snapshot.json 引用的每个 blob(entries.copiedTo / hostFileBackups.blobPath)都必须被 manifest 覆盖,
@@ -770,7 +800,7 @@ export async function verifySnapshot(snapshotsDir: string, id: string): Promise<
770
800
  }
771
801
  for (const relBlob of referencedBlobs) {
772
802
  if (!(relBlob in manifest.blobHashes)) {
773
- return { ok: false, reason: `snapshot 引用的 blob 未在 manifest 中: ${relBlob}` };
803
+ return { ok: false, reason: engineMsg('core.backup.blobNotInManifest', { blob: relBlob }) };
774
804
  }
775
805
  }
776
806
  return { ok: true };
@@ -35,6 +35,7 @@ import path from 'node:path';
35
35
 
36
36
  import { atomicWriteFile } from '../utils/atomic-write.ts';
37
37
  import { sha256Hex } from '../utils/hashing.ts';
38
+ import { engineMsg } from './messages-core.ts';
38
39
 
39
40
  /* ---------------------------------------------------------------- 常量 */
40
41
 
@@ -414,14 +415,14 @@ export interface RescueSelfMount {
414
415
  /** 救援模式 profile patch 的头注释(说明本文件被谁接管、bundle 层的实际状态)。 */
415
416
  const RESCUE_PATCH_HEADER_LINES = [
416
417
  '# dsh-config-manager RESCUE MODE',
417
- '# 本文件(profile patch 层)已被救援模式接管;home patch 同时被置空。',
418
+ engineMsg('core.bootRescue.patchTakenOver'),
418
419
  ];
419
420
 
420
421
  /** 救援模式 profile patch 的头注释(按 bundle 处理方式给出准确说明,不得夸大能力)。 */
421
422
  function rescuePatchHeader(bundlesDisabled: boolean): string {
422
423
  const note = bundlesDisabled
423
- ? '# dsh.profile.bundles 已被收窄为 DSH 核心与救援插件自身,被禁用的包同时已从 dependencies 临时摘除:其余用户插件本次启动不挂载。'
424
- : '# 注意:dsh.profile.bundles 里的插件仍由 bundle 层挂载(本次只裁剪了不可解析的 bundle)。';
424
+ ? engineMsg('core.bootRescue.bundlesNarrowed')
425
+ : engineMsg('core.bootRescue.bundlesNote');
425
426
  return [...RESCUE_PATCH_HEADER_LINES, note, ''].join('\n');
426
427
  }
427
428
 
@@ -23,6 +23,7 @@ import type { BundleResolver } from './boot-rescue.ts';
23
23
  import { BOOT_CRITICAL_RELS, profileCriticalRels } from './boot-paths.ts';
24
24
  import type { MsgFunc } from './messages.ts';
25
25
  import { zhMsg } from './messages.ts';
26
+ import { engineMsg } from './messages-core.ts';
26
27
 
27
28
  /** 审计问题 id(稳定;测试与 UI 判定用,展示文本一律走 msg)。 */
28
29
  export type BootSafetyIssueId =
@@ -167,7 +168,7 @@ export async function auditBootSafety(deps: BootSafetyDeps): Promise<BootSafetyR
167
168
  // 只剔除「不可解析且非核心」的条目:其余条目(含解析不出的核心项)原样保留
168
169
  const drop = new Set(prunable.map((p) => p.name))
169
170
  const next = (Array.isArray(bundles) ? bundles : []).filter((b) => !(typeof b === 'string' && drop.has(b))) as string[]
170
- if (!writeBundles(pkg, next)) throw new Error('dsh.profile.bundles 不是数组或结构异常')
171
+ if (!writeBundles(pkg, next)) throw new Error(engineMsg('core.bootSafety.bundlesNotArray'))
171
172
  await deps.writeText(pkgRel, JSON.stringify(pkg, null, 2) + '\n')
172
173
  written = true
173
174
  } catch (err) {