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
@@ -0,0 +1,87 @@
1
+ /**
2
+ * security 层的领域消息目录(zh 源 / en 镜像)—— 经 engineMsg 取「调用期」语言文案。
3
+ *
4
+ * 为什么是本域独立目录:src/security/** 的写作用域不含 src/core/messages.ts(既有键有冻结指纹),
5
+ * 所以本域的键用 registerMessageCatalog 在**模块加载时**注册进引擎目录,查找顺序见
6
+ * src/core/engine-locale.ts(基础目录 → 注册目录 → 键名)。
7
+ *
8
+ * 纪律(与 src/core/messages.ts 同口径):
9
+ * - zh 值 = 迁移前的既有中文文案**逐字不变**(既有测试断言依赖);
10
+ * - en 类型为 Record<keyof typeof zh, string> —— 缺键/多键即编译错误;
11
+ * - {param} 插值与缺参保留由 engineMsg 负责;
12
+ * - **凭据 / 密码值永不进消息**:这里只换文案,脱敏口径与拼接顺序逐字不变(转写前长什么样,
13
+ * 转写后还是什么样),绝不新增任何值参数。
14
+ */
15
+ import { engineMsg, registerMessageCatalog } from '../core/engine-locale.ts';
16
+ import type { MsgParams } from '../core/msg-types.ts';
17
+
18
+ export const zh = {
19
+ // ---------- 加密容器(encryption.ts) ----------
20
+ 'security.encryptionPasswordEmpty': '加密密码不能为空',
21
+ 'security.secretsBlobTooSmall': 'secrets.enc 体积过小(截断或损坏)',
22
+ 'security.unknownFileMagic': '未知文件格式 magic "{magic}",不是本插件产物',
23
+ 'security.unsupportedSecretsVersion': '不支持的 secrets.enc 版本 {version}',
24
+ 'security.invalidEncryptionParams': 'manifest 加密参数非法或不受支持',
25
+ 'security.encryptionParamsMismatch': 'secrets.enc 与 manifest 加密参数不一致(可能被篡改)',
26
+ 'security.keyDerivationFailed': '密钥派生失败: {reason}',
27
+ 'security.decryptAuthFailed': '解密认证失败:密码错误或密文被篡改',
28
+ 'security.archiveBlobTooSmall': '加密备份容器体积过小(截断或损坏)',
29
+ 'security.unsupportedArchiveVersion': '不支持的加密备份版本 {version}',
30
+ 'security.archiveParamsMismatch': '加密备份参数不一致(可能被篡改)',
31
+
32
+ // ---------- 完整性校验表(integrity.ts) ----------
33
+ 'security.checksumsMustBeJsonObject': 'checksums.json 必须是 JSON 对象',
34
+ 'security.checksumsTooManyEntries': 'checksums.json 条目数 {count} 超过上限 {max}',
35
+ 'security.checksumsDangerousKey': 'checksums.json 含危险键 "{path}",已拒绝',
36
+ 'security.checksumsIllegalPath': 'checksums.json 含非法相对路径 "{path}"',
37
+ 'security.checksumsNotSha256': 'checksums.json["{path}"] 不是合法的 SHA-256 hex',
38
+ 'security.checksumMismatch': '"{path}" (hash 不符)',
39
+ 'security.checksumMissing': '"{path}" (缺失)',
40
+
41
+ // ---------- 秘密扫描(secret-scanner.ts) ----------
42
+ 'security.scanDepthExceeded': '扫描深度 {depth} 超过上限 {max}',
43
+ 'security.circularReference': '检测到循环引用,无法扫描(路径 {path})',
44
+
45
+ // ---------- vault / ZIP 安全(vault.ts / zip-security.ts) ----------
46
+ 'security.vaultUnsafePath': 'vault: 非法敏感文件路径 "{path}"',
47
+ 'security.zipTargetOutOfBounds': '解压目标越界: {name}',
48
+ 'security.zipExecutableEntry': '条目 "{name}" 是潜在可执行文件,本插件不会执行它',
49
+ 'security.zipEntryNotRegularFile': '解压产物不是普通文件: {rel}',
50
+ } as const;
51
+
52
+ export const en: Record<keyof typeof zh, string> = {
53
+ 'security.encryptionPasswordEmpty': 'Encryption password must not be empty',
54
+ 'security.secretsBlobTooSmall': 'secrets.enc is too small (truncated or corrupted)',
55
+ 'security.unknownFileMagic': 'Unknown file format magic "{magic}"; not a product of this plugin',
56
+ 'security.unsupportedSecretsVersion': 'Unsupported secrets.enc version {version}',
57
+ 'security.invalidEncryptionParams': 'Invalid or unsupported manifest encryption parameters',
58
+ 'security.encryptionParamsMismatch': 'secrets.enc encryption parameters do not match the manifest (possibly tampered with)',
59
+ 'security.keyDerivationFailed': 'Key derivation failed: {reason}',
60
+ 'security.decryptAuthFailed': 'Decryption authentication failed: wrong password or tampered ciphertext',
61
+ 'security.archiveBlobTooSmall': 'Encrypted backup container is too small (truncated or corrupted)',
62
+ 'security.unsupportedArchiveVersion': 'Unsupported encrypted backup version {version}',
63
+ 'security.archiveParamsMismatch': 'Encrypted backup parameters do not match (possibly tampered with)',
64
+
65
+ 'security.checksumsMustBeJsonObject': 'checksums.json must be a JSON object',
66
+ 'security.checksumsTooManyEntries': 'checksums.json has {count} entries, exceeding the limit of {max}',
67
+ 'security.checksumsDangerousKey': 'checksums.json contains a dangerous key "{path}", rejected',
68
+ 'security.checksumsIllegalPath': 'checksums.json contains an illegal relative path "{path}"',
69
+ 'security.checksumsNotSha256': 'checksums.json["{path}"] is not a valid SHA-256 hex string',
70
+ 'security.checksumMismatch': '"{path}" (hash mismatch)',
71
+ 'security.checksumMissing': '"{path}" (missing)',
72
+
73
+ 'security.scanDepthExceeded': 'Scan depth {depth} exceeds the limit of {max}',
74
+ 'security.circularReference': 'Circular reference detected, cannot scan (path {path})',
75
+
76
+ 'security.vaultUnsafePath': 'vault: illegal sensitive file path "{path}"',
77
+ 'security.zipTargetOutOfBounds': 'Extraction target escapes the destination: {name}',
78
+ 'security.zipExecutableEntry': 'Entry "{name}" is a potential executable; this plugin will not execute it',
79
+ 'security.zipEntryNotRegularFile': 'Extracted entry is not a regular file: {rel}',
80
+ };
81
+
82
+ registerMessageCatalog({ id: 'security', zh, en });
83
+
84
+ /** 本域翻译器:key 受目录约束(拼错键 = 编译错误),语言按 engineMsg 的活语言解析。 */
85
+ export function securityMsg(key: keyof typeof zh, params?: MsgParams): string {
86
+ return engineMsg(key, params);
87
+ }
@@ -18,7 +18,7 @@
18
18
  import test from 'node:test';
19
19
  import assert from 'node:assert/strict';
20
20
 
21
- import { REDACTION_VALUE_PATTERNS, REDACTED, redact } from './redaction.ts';
21
+ import { REDACTION_VALUE_PATTERNS, REDACTED, createRedactor, redact } from './redaction.ts';
22
22
  import { SECRET_VALUE_PATTERNS, matchSecretValuePattern } from './secret-scanner.ts';
23
23
 
24
24
  /** 真实凭据形态语料(scanner 必须命中;redact 必须掩掉原文) */
@@ -147,3 +147,19 @@ test('t78-f(**t83 后范围收窄**):显示层「更宽」只对未加形
147
147
  assert.equal(matchSecretValuePattern('Bearer example-token-here'), null, 'scanner 侧放行(示例形态)');
148
148
  assert.ok(redact('Bearer example-token-here').includes(REDACTED), 'redact 侧仍掩(显示层更宽,未变)');
149
149
  });
150
+
151
+ /* ---------------- URL 内嵌凭据(userinfo 形态)必须掩码 ---------------- */
152
+
153
+ test('redact:scheme://user:password@host 的密码段被掩码(用户名保留),且幂等', () => {
154
+ const out = redact('clone https://oauth2:GLPAT-SECRETVALUE@gitlab.example.com/u/r.git now');
155
+ assert.equal(out.includes('GLPAT-SECRETVALUE'), false, 'URL 内嵌凭据不得明文保留');
156
+ assert.ok(out.includes('oauth2'), '用户名保留可读(与 URL_QUERY_RE 同一取舍)');
157
+ assert.ok(out.includes('gitlab.example.com'), '主机名不受影响');
158
+ assert.equal(redact(out), out, '幂等:重复脱敏结果不变');
159
+ // query 形态的既有行为不变
160
+ assert.equal(redact('https://h/p?token=abc123456').includes('abc123456'), false, 'query token 仍被掩码');
161
+ // 没有 userinfo 的普通 URL 一字不改
162
+ assert.equal(redact('https://gitlab.example.com/u/r.git'), 'https://gitlab.example.com/u/r.git');
163
+ // createRedactor 与 redact 同源(同一份规则)
164
+ assert.equal(createRedactor()('https://u:p4ssword@h/x').includes('p4ssword'), false);
165
+ });
@@ -54,6 +54,17 @@ const VALUE_PATTERNS = REDACTION_VALUE_PATTERNS;
54
54
  /** URL query 中的敏感参数值(保留参数名,只替换值) */
55
55
  const URL_QUERY_RE = /([?&](?:token|api[_-]?key|key|secret|access[_-]?token|password|auth)=)([^&\s"']+)/gi;
56
56
 
57
+ /**
58
+ * URL **内嵌凭据**(\`scheme://user:password@host\`,GitLab/Gitea 常见的 \`https://oauth2:<token>@…\`)。
59
+ *
60
+ * 为什么必须补这一条:URL_QUERY_RE 只认 query 参数,而 userinfo 形态的凭据此前在**三条通道**
61
+ * 都不被掩码 —— 日志、界面渲染、以及 sessionStorage 落盘(run-store 的 \`redactPersistedValue\`)。
62
+ * 宿主侧 sync-config 的 validateRepoUrl/validateWebDavUrl 本来就拒绝带 userinfo 的地址,
63
+ * 所以这里只影响「掩码」:不改任何合法输入的行为,只让非法规格里的凭据不再明文可见。
64
+ * 只替换密码段(用户名保留可读),与 URL_QUERY_RE 的取舍一致。
65
+ */
66
+ const URL_USERINFO_RE = /(\/\/[^\/@\s:]+:)([^\/@\s]+)(@)/g;
67
+
57
68
  /* ---------------- 高熵长 token(journal / 迁移历史强脱敏共用) ---------------- */
58
69
 
59
70
  /** 高熵值形状(长 hex/base64/随机 id):结构化 redact() 覆盖不了任意 secret,journal / 迁移
@@ -120,6 +131,7 @@ export function redact(text: string, blacklist: readonly string[] = []): string
120
131
  out = out.replace(re, REDACTED);
121
132
  }
122
133
  out = out.replace(URL_QUERY_RE, `$1${REDACTED}`);
134
+ out = out.replace(URL_USERINFO_RE, `$1${REDACTED}$3`);
123
135
  return out;
124
136
  }
125
137
 
@@ -147,6 +159,7 @@ export function createRedactor(blacklist: readonly string[] = []): (text: string
147
159
  out = out.replace(re, REDACTED);
148
160
  }
149
161
  out = out.replace(URL_QUERY_RE, `$1${REDACTED}`);
162
+ out = out.replace(URL_USERINFO_RE, `$1${REDACTED}$3`);
150
163
  return out;
151
164
  };
152
165
  }
@@ -22,10 +22,13 @@
22
22
  * - 高熵 base64 默认关(误伤率高)。
23
23
  *
24
24
  * 实现不变量:返回 sanitized 为**全新对象**(不改原数据);迭代式显式栈(防爆栈)+ 深度上限;
25
+ * 判环只看**当前路径的祖先链**(`ancestors`)——同一对象被两处引用(DAG)不是环,用全局 visited
26
+ * 会把共享引用误判成循环引用并让整次导出失败;
25
27
  * 命中记录只含路径与字段名,**值永不外泄**。
26
28
  */
27
29
  import { JsonDepthError } from '../utils/json.ts';
28
30
  import type { SecretScanner, SensitiveHit } from '../core/types.ts';
31
+ import { securityMsg } from './messages.ts';
29
32
 
30
33
  /** 剥离占位(与 core defaultSecretScanner 语义一致:空串 = 需补录提示) */
31
34
  export const REDACTED_PLACEHOLDER = '';
@@ -351,15 +354,17 @@ export function scanAndRedact(data: unknown, opts: SecretScannerOptions = {}): {
351
354
  if (data instanceof Uint8Array) return { sanitized: data, hits };
352
355
 
353
356
  const rootOut: unknown = Array.isArray(data) ? [] : {};
354
- interface Frame { src: unknown; dst: unknown; path: string; depth: number }
355
- const stack: Frame[] = [{ src: data, dst: rootOut, path: '', depth: 1 }];
356
- const visited = new WeakSet<object>();
357
- visited.add(data as object);
357
+ // 判环判据 = **当前路径的祖先链**,绝不是「本次扫描见过的对象集合」:settings 的 value/base 共享
358
+ // 未覆盖的嵌套分组、providers 的多 route 共享 raw 等 DAG 形态都是合法数据,全局 visited 会把它们
359
+ // 误判成「检测到循环引用」并让整次导出/同步失败(用户上报 2026-10-09:备份失败 → intent journal
360
+ // 记 NEEDS_ATTENTION + durable SAFE MODE)。祖先链只回答「它是不是它自己的祖先」。
361
+ interface Frame { src: unknown; dst: unknown; path: string; depth: number; ancestors: ReadonlySet<object> }
362
+ const stack: Frame[] = [{ src: data, dst: rootOut, path: '', depth: 1, ancestors: new Set<object>([data as object]) }];
358
363
 
359
364
  while (stack.length > 0) {
360
365
  const frame = stack.pop()!;
361
- const { src, dst, path, depth } = frame;
362
- if (depth > maxDepth) throw new JsonDepthError(`扫描深度 ${depth} 超过上限 ${maxDepth}`);
366
+ const { src, dst, path, depth, ancestors } = frame;
367
+ if (depth > maxDepth) throw new JsonDepthError(securityMsg('security.scanDepthExceeded', { depth, max: maxDepth }));
363
368
  if (src instanceof Uint8Array) continue;
364
369
 
365
370
  if (Array.isArray(src)) {
@@ -368,11 +373,12 @@ export function scanAndRedact(data: unknown, opts: SecretScannerOptions = {}): {
368
373
  const v = src[i];
369
374
  const childPath = `${path}[${i}]`;
370
375
  if (v !== null && typeof v === 'object' && !(v instanceof Uint8Array)) {
371
- if (visited.has(v)) throw new Error(`检测到循环引用,无法扫描(路径 ${childPath})`);
372
- visited.add(v);
376
+ if (ancestors.has(v)) throw new Error(securityMsg('security.circularReference', { path: childPath }));
377
+ const childAncestors = new Set(ancestors);
378
+ childAncestors.add(v);
373
379
  const childOut: unknown = Array.isArray(v) ? [] : {};
374
380
  outArr.push(childOut);
375
- stack.push({ src: v, dst: childOut, path: childPath, depth: depth + 1 });
381
+ stack.push({ src: v, dst: childOut, path: childPath, depth: depth + 1, ancestors: childAncestors });
376
382
  } else {
377
383
  outArr.push(v);
378
384
  }
@@ -393,11 +399,12 @@ export function scanAndRedact(data: unknown, opts: SecretScannerOptions = {}): {
393
399
  continue;
394
400
  }
395
401
  if (v !== null && typeof v === 'object' && !(v instanceof Uint8Array)) {
396
- if (visited.has(v)) throw new Error(`检测到循环引用,无法扫描(路径 ${childPath})`);
397
- visited.add(v);
402
+ if (ancestors.has(v)) throw new Error(securityMsg('security.circularReference', { path: childPath }));
403
+ const childAncestors = new Set(ancestors);
404
+ childAncestors.add(v);
398
405
  const childOut: unknown = Array.isArray(v) ? [] : {};
399
406
  outObj[k] = childOut;
400
- stack.push({ src: v, dst: childOut, path: childPath, depth: depth + 1 });
407
+ stack.push({ src: v, dst: childOut, path: childPath, depth: depth + 1, ancestors: childAncestors });
401
408
  } else {
402
409
  outObj[k] = v;
403
410
  }
@@ -727,6 +727,53 @@ test('scanner: 深层嵌套上限(≤64 通过,超限抛错)与循环引
727
727
  assert.throws(() => scanAndRedact(cyclic), /循环引用/);
728
728
  });
729
729
 
730
+ test('scanner: DAG 共享引用不得被误判为循环(用户上报 2026-10-09:settings value/base 共享 → 备份整体失败)', () => {
731
+ // 同一对象被两处引用(彼此不是祖先关系)是合法数据:settings 的 value/base 共享未覆盖的嵌套分组、
732
+ // providers 的多 route 共享 raw 都长这样。判环只看祖先链 → 放行,且两个分支各自完成脱敏。
733
+ const dream = { enabled: true, idleMinutes: 180, apiKey: 'sk-live-abcdefgh12345' };
734
+ const data = {
735
+ namespaces: {
736
+ 'meow-memory': {
737
+ value: { promptLang: 'zh', dream },
738
+ base: { promptLang: 'zh', dream },
739
+ revision: 3,
740
+ secrets: [],
741
+ },
742
+ },
743
+ };
744
+ const { sanitized, hits } = scanAndRedact(data);
745
+ const rec = (sanitized as {
746
+ namespaces: Record<string, { value: { dream: Record<string, unknown> }; base: { dream: Record<string, unknown> } }>;
747
+ }).namespaces['meow-memory']!;
748
+ assert.equal(rec.value.dream['apiKey'], REDACTED_PLACEHOLDER, 'value 分支必须脱敏');
749
+ assert.equal(rec.base.dream['apiKey'], REDACTED_PLACEHOLDER, 'base 分支必须脱敏');
750
+ assert.equal(rec.value.dream['idleMinutes'], 180, '非敏感字段保留');
751
+ assert.equal(rec.base.dream['idleMinutes'], 180);
752
+ assert.notStrictEqual(rec.value.dream, rec.base.dream, '输出必须是各自独立的副本');
753
+ assert.ok(hits.some((h) => h.path === 'namespaces.meow-memory.value.dream.apiKey'), '命中路径须点名 value 分支');
754
+ assert.ok(hits.some((h) => h.path === 'namespaces.meow-memory.base.dream.apiKey'), '命中路径须点名 base 分支');
755
+ });
756
+
757
+ test('scanner: 数组分支的共享引用放行,真环(对象环 / 数组环 / 深层回指)仍必须抛', () => {
758
+ const shared = { token: 'ghp_abcdefghijklmnopqrstuvwxyz012345' };
759
+ const { sanitized } = scanAndRedact({ a: { x: shared }, b: { y: shared }, c: { z: shared } });
760
+ const s = sanitized as { a: { x: { token: string } }; b: { y: { token: string } }; c: { z: { token: string } } };
761
+ assert.equal(s.a.x.token, REDACTED_PLACEHOLDER);
762
+ assert.equal(s.b.y.token, REDACTED_PLACEHOLDER);
763
+ assert.equal(s.c.z.token, REDACTED_PLACEHOLDER);
764
+ assert.notStrictEqual(s.a.x, s.b.y, '三处共享引用必须各写一份独立副本');
765
+
766
+ // 数组自环
767
+ const arrayCycle: unknown[] = [1];
768
+ arrayCycle.push(arrayCycle);
769
+ assert.throws(() => scanAndRedact({ x: arrayCycle }), /循环引用/);
770
+
771
+ // 同一对象既是自己的祖先(隔着若干层回指)→ 仍必须抛,不能因为「见过」就放行
772
+ const root: Record<string, unknown> = {};
773
+ root['deep'] = { mid: { back: root } };
774
+ assert.throws(() => scanAndRedact(root), /循环引用/);
775
+ });
776
+
730
777
  test('scanner: Uint8Array 跳过、数组根、空字符串不报 hit、extra 名单扩展', () => {
731
778
  const bin = new Uint8Array([1, 2, 3]);
732
779
  const input = { data: bin, empty: '', clientSecret2: 'x', normal: 42 };
@@ -23,6 +23,7 @@
23
23
  import path from 'node:path';
24
24
  import type { FileSystemFacade } from '../core/types.ts';
25
25
  import { isPathSafe, normalizePath } from '../utils/paths.ts';
26
+ import { securityMsg } from './messages.ts';
26
27
 
27
28
  /**
28
29
  * 默认敏感文件清单(相对 $DSH_HOME):当前 DSH 唯一「整文件即秘密」的文件
@@ -68,7 +69,7 @@ export interface VaultEntry {
68
69
 
69
70
  /** rel 必须为安全相对路径(拒绝绝对路径 / .. / 盘符 / NUL),防路径穿越 */
70
71
  function assertSafeRel(rel: string): void {
71
- if (!isPathSafe(rel)) throw new Error(`vault: 非法敏感文件路径 "${rel}"`);
72
+ if (!isPathSafe(rel)) throw new Error(securityMsg('security.vaultUnsafePath', { path: rel }));
72
73
  }
73
74
 
74
75
  /** 校验 + 归一化 rel 清单(统一 / 分隔,与 FileSystemFacade.listRecursive 产出对齐) */
@@ -21,6 +21,7 @@ import {
21
21
  ZipSafetyError,
22
22
  type ZipSafetyLimits,
23
23
  } from '../utils/zip.ts';
24
+ import { securityMsg } from './messages.ts';
24
25
 
25
26
  export { isPathSafe } from '../utils/paths.ts';
26
27
 
@@ -83,13 +84,13 @@ export async function safeExtractHardened(
83
84
  if (meta.isDirectory) continue;
84
85
  const target = path.join(destDir, ...meta.name.split('/'));
85
86
  if (!isSameOrChild(target, destDir)) {
86
- throw new ZipSafetyError(`解压目标越界: ${meta.name}`);
87
+ throw new ZipSafetyError(securityMsg('security.zipTargetOutOfBounds', { name: meta.name }));
87
88
  }
88
89
  await fs.mkdir(path.dirname(target), { recursive: true });
89
90
  await fs.writeFile(target, archive.readEntry(meta.name));
90
91
  extracted.push(meta.name);
91
92
  if (isExecutableName(meta.name)) {
92
- warnings.push(`条目 "${meta.name}" 是潜在可执行文件,本插件不会执行它`);
93
+ warnings.push(securityMsg('security.zipExecutableEntry', { name: meta.name }));
93
94
  }
94
95
  }
95
96
  // 解压后复查:全部产物必须是普通文件(防 symlink 逃逸 / 非常规写入)
@@ -97,7 +98,7 @@ export async function safeExtractHardened(
97
98
  const p = path.join(destDir, ...rel.split('/'));
98
99
  const st = await fs.lstat(p);
99
100
  if (!st.isFile()) {
100
- throw new ZipSafetyError(`解压产物不是普通文件: ${rel}`);
101
+ throw new ZipSafetyError(securityMsg('security.zipEntryNotRegularFile', { rel }));
101
102
  }
102
103
  }
103
104
  return { files: extracted, warnings };
@@ -4,6 +4,7 @@
4
4
  * 纯逻辑 + 可注入 fs;不触碰真实 ~/.dsh。
5
5
  */
6
6
  import { createSnapshotFs, joinFs } from './fs.ts';
7
+ import { engineMsg } from './messages.ts';
7
8
  import type { SnapshotFs } from './fs.ts';
8
9
  import { readSnapshotFromDir, writeSnapshotToDir } from './layout.ts';
9
10
  import { SNAPSHOT_MANIFEST_FILE } from './layout.ts';
@@ -19,7 +20,7 @@ export async function loadAncestor(
19
20
  fsx: SnapshotFs = createSnapshotFs(),
20
21
  ): Promise<SyncSnapshot> {
21
22
  if (typeof snapshotId !== 'string' || snapshotId === '') {
22
- throw new Error('loadAncestor: snapshotId 不能为空');
23
+ throw new Error(engineMsg('sync.ancestorSnapshotIdRequired'));
23
24
  }
24
25
  const dir = joinFs(localSnapshotsDir, snapshotId);
25
26
  return await readSnapshotFromDir(dir, fsx);
@@ -52,6 +52,17 @@ export interface AutosyncConfig {
52
52
  /** 最近一次自动同步执行状态 */
53
53
  lastRunStatus?: AutosyncRunStatus;
54
54
  lastRunMessage?: string;
55
+ /**
56
+ * 最近一次**跳过**的结构化原因码(issue #79)。
57
+ *
58
+ * 为什么是码而不是文案:语言在「取文案那一刻」解析(AGENTS.md i18n 铁律),状态行由界面
59
+ * 按码取自己的字典;存一句当时语言的中文,用户切到英文界面后会看到中英混排。
60
+ * 已知值见 `svc.autosyncSkip.*` 的字典键('conflict'/'mutation-locked'/'upToDate'…),
61
+ * **未知码一律渲染兜底文案,绝不渲染裸枚举**。
62
+ */
63
+ lastRunSkipReason?: string;
64
+ /** 最近一次跳过涉及的分区数(当前仅 conflict:远端另有 N 个分区待确认) */
65
+ lastRunSkipCount?: number;
55
66
  /** 最近一次自动同步触发的同步历史条目 id */
56
67
  lastRunHistoryId?: string;
57
68
  }
@@ -85,6 +96,13 @@ function parseV1Channel(obj: Record<string, unknown>): AutosyncConfig {
85
96
  cfg.lastRunStatus = obj['lastRunStatus'];
86
97
  }
87
98
  if (typeof obj['lastRunMessage'] === 'string') cfg.lastRunMessage = obj['lastRunMessage'];
99
+ // issue #79:状态行的结构化跳过原因(旧文件没有这两个字段 → undefined,行为不变)
100
+ if (typeof obj['lastRunSkipReason'] === 'string' && obj['lastRunSkipReason'] !== '') {
101
+ cfg.lastRunSkipReason = obj['lastRunSkipReason'];
102
+ }
103
+ if (typeof obj['lastRunSkipCount'] === 'number' && Number.isFinite(obj['lastRunSkipCount']) && obj['lastRunSkipCount'] >= 0) {
104
+ cfg.lastRunSkipCount = obj['lastRunSkipCount'];
105
+ }
88
106
  if (typeof obj['lastRunHistoryId'] === 'string') cfg.lastRunHistoryId = obj['lastRunHistoryId'];
89
107
  return cfg;
90
108
  }
@@ -161,6 +179,9 @@ function toChannelPayload(cfg: AutosyncConfig): Record<string, unknown> {
161
179
  if (cfg.lastRunAt !== undefined && cfg.lastRunAt !== '') payload['lastRunAt'] = cfg.lastRunAt;
162
180
  if (cfg.lastRunStatus !== undefined) payload['lastRunStatus'] = cfg.lastRunStatus;
163
181
  if (cfg.lastRunMessage !== undefined && cfg.lastRunMessage !== '') payload['lastRunMessage'] = cfg.lastRunMessage;
182
+ // 空串/undefined 一律不写 → 本轮成功时,上一轮的跳过码/旧消息在文件里被真正清掉(issue #79)
183
+ if (cfg.lastRunSkipReason !== undefined && cfg.lastRunSkipReason !== '') payload['lastRunSkipReason'] = cfg.lastRunSkipReason;
184
+ if (cfg.lastRunSkipCount !== undefined) payload['lastRunSkipCount'] = cfg.lastRunSkipCount;
164
185
  if (cfg.lastRunHistoryId !== undefined && cfg.lastRunHistoryId !== '') payload['lastRunHistoryId'] = cfg.lastRunHistoryId;
165
186
  return payload;
166
187
  }
@@ -283,8 +283,10 @@ test('runOnce: webdav 未配置(webdav.url 缺)→ skipped(unconfigured),
283
283
  const result = await scheduler.runOnce('webdav');
284
284
  assert.equal(result.status, 'skipped');
285
285
  assert.equal(result.skipReason, 'unconfigured');
286
- assert.equal(result.consecutiveFailures, 1, '未配置不计失败(保持 1)');
287
- assert.equal(getConfig().consecutiveFailures, 1);
286
+ // issue #79:连续失败计数只统计「真的失败」——未配置属跳过,必须归零(此前保持 1,
287
+ // 面板于是长期显示「连续失败 N 次」,把用户引向网络排查)
288
+ assert.equal(result.consecutiveFailures, 0, '未配置不算失败 → 计数归零');
289
+ assert.equal(getConfig().consecutiveFailures, 0);
288
290
  });
289
291
 
290
292
  test('syncIsConfigured: git 看 git.repoUrl、webdav 看 webdav.url;null 未配置', () => {
@@ -306,19 +308,73 @@ test('runOnce: merge 抛错 → failed,连续失败计数 +1', async () => {
306
308
  assert.ok(getEntries().some((e) => e.status === 'failed'), '写入失败历史');
307
309
  });
308
310
 
309
- test('runOnce: 有冲突 → skipped(conflict),不写本地,不计失败', async () => {
311
+ test('issue #79:远端冲突只挡下行 —— 不写本地,但仍推送本机改动(partial,不谎报 success/failed)', async () => {
310
312
  const cfg: AutosyncConfig = { enabled: true, interval: '30m', startupMinIntervalMs: 300000, consecutiveFailures: 1 };
313
+ const applyCalls: string[] = [];
314
+ const pushCalls: string[] = [];
311
315
  const engine = {
316
+ hasNewRemoteSnapshot: async () => true,
317
+ hasLocalChanges: async () => true,
312
318
  merge: async (): Promise<MergePlan> => makeMergePlan([['settings', 'conflict']]),
319
+ applyMergePlan: async () => {
320
+ applyCalls.push('apply');
321
+ return { ok: true, applied: [] as never[], restoreId: 'r1', rolledBack: false, review: [], warnings: [] };
322
+ },
323
+ push: async () => { pushCalls.push('push'); return { ok: true, snapshotId: 'snap-up', sections: [] as never, warnings: [] }; },
313
324
  };
314
325
  const { scheduler, getConfig, getEntries } = makeScheduler({ cfg, engine, history: [] });
315
326
  const result = await scheduler.runOnce('git');
316
- assert.equal(result.status, 'skipped');
327
+ assert.equal(result.status, 'partial', '上行成功 + 下行待确认 → partial');
317
328
  assert.equal(result.skipReason, 'conflict');
318
329
  assert.deepEqual(result.conflictedSections, ['settings']);
319
- assert.equal(result.consecutiveFailures, 1, '冲突跳过不计失败');
320
- assert.equal(getConfig().consecutiveFailures, 1);
321
- assert.ok(getEntries().some((e) => e.skipReason === 'conflict'), '写入冲突跳过历史');
330
+ assert.equal(result.pushedSnapshotId, 'snap-up', '本机快照仍有上行出口(issue #79 主诉)');
331
+ assert.equal(applyCalls.length, 0, '冲突绝不写本机配置');
332
+ assert.equal(pushCalls.length, 1, '纯上行不被冲突连坐');
333
+ assert.equal(result.consecutiveFailures, 0, '冲突跳过不是失败 → 计数归零');
334
+ assert.equal(getConfig().lastRunSkipReason, 'conflict', '跳过原因落到状态(界面按码取字典)');
335
+ assert.equal(getConfig().lastRunSkipCount, 1);
336
+ assert.ok(getEntries().some((e) => e.status === 'partial' && e.skipReason === 'conflict'), '写入 partial 历史');
337
+ assert.ok(getEntries().some((e) => e.pushedSnapshotId === 'snap-up'), '历史带上行快照 id');
338
+ });
339
+
340
+ test('issue #79:冲突且本机无改动(非 startup)→ skipped(conflict) 且不带 push', async () => {
341
+ const cfg: AutosyncConfig = { enabled: true, interval: '30m', startupMinIntervalMs: 300000, consecutiveFailures: 0 };
342
+ const pushCalls: string[] = [];
343
+ const engine = {
344
+ hasNewRemoteSnapshot: async () => true,
345
+ hasLocalChanges: async () => false,
346
+ merge: async (): Promise<MergePlan> => makeMergePlan([['settings', 'conflict']]),
347
+ push: async () => { pushCalls.push('push'); return { ok: true, snapshotId: 'x', sections: [] as never, warnings: [] }; },
348
+ };
349
+ const { scheduler, getConfig } = makeScheduler({ cfg, engine, history: [] });
350
+ const result = await scheduler.runOnce('git');
351
+ assert.equal(result.status, 'skipped');
352
+ assert.equal(result.skipReason, 'conflict');
353
+ assert.equal(pushCalls.length, 0, '本地无改动不上传');
354
+ assert.equal(getConfig().lastRunMessage ?? '', '', '本轮跳过不得残留上一轮的错误文案');
355
+ assert.equal(getConfig().lastRunSkipReason, 'conflict');
356
+ });
357
+
358
+ test('issue #79:本轮成功必须清掉上一轮的错误文案与跳过码(陈旧信息不再挂面板)', async () => {
359
+ const cfg: AutosyncConfig = {
360
+ enabled: true, interval: '30m', startupMinIntervalMs: 300000, consecutiveFailures: 2,
361
+ lastRunMessage: 'git pull 失败: ssh: connect to host 192.168.10.x port 2222: Connection timed out',
362
+ lastRunSkipReason: 'mutation-locked',
363
+ lastRunSkipCount: 7,
364
+ };
365
+ const engine = {
366
+ hasNewRemoteSnapshot: async () => false,
367
+ hasLocalChanges: async () => true,
368
+ push: async () => ({ ok: true, snapshotId: 'snap-clean', sections: [] as never, warnings: [] }),
369
+ };
370
+ const { scheduler, getConfig } = makeScheduler({ cfg, engine, history: [] });
371
+ const result = await scheduler.runOnce('git');
372
+ assert.equal(result.status, 'success');
373
+ const saved = getConfig();
374
+ assert.equal(saved.lastRunMessage, '', '成功即清空旧错误文案');
375
+ assert.equal(saved.lastRunSkipReason, undefined, '成功即清空旧跳过码');
376
+ assert.equal(saved.lastRunSkipCount, undefined);
377
+ assert.equal(saved.consecutiveFailures, 0, '成功归零连续失败');
322
378
  });
323
379
 
324
380
  test('runOnce: 无冲突且无变化 → success(pull,unchanged),不上传(本地也无改动)', async () => {