dsh-config-manager 0.1.63 → 0.1.64

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 (467) hide show
  1. package/README.md +35 -8
  2. package/README.zh-CN.md +12 -7
  3. package/lib/adapters/agent-instructions.d.ts +3 -3
  4. package/lib/adapters/agent-instructions.js +5 -3
  5. package/lib/adapters/agent-presets.d.ts +3 -3
  6. package/lib/adapters/agent-presets.js +5 -3
  7. package/lib/adapters/credentials.d.ts +3 -3
  8. package/lib/adapters/credentials.js +20 -23
  9. package/lib/adapters/file-collection.d.ts +43 -0
  10. package/lib/adapters/file-collection.js +130 -18
  11. package/lib/adapters/index.d.ts +1 -0
  12. package/lib/adapters/index.js +11 -0
  13. package/lib/adapters/json-section.d.ts +32 -0
  14. package/lib/adapters/json-section.js +61 -0
  15. package/lib/adapters/mcp.d.ts +3 -3
  16. package/lib/adapters/mcp.js +16 -19
  17. package/lib/adapters/plugin-files.d.ts +3 -3
  18. package/lib/adapters/plugin-files.js +11 -14
  19. package/lib/adapters/plugins.d.ts +3 -3
  20. package/lib/adapters/plugins.js +39 -42
  21. package/lib/adapters/prompts.d.ts +3 -3
  22. package/lib/adapters/prompts.js +16 -20
  23. package/lib/adapters/providers.d.ts +3 -3
  24. package/lib/adapters/providers.js +23 -26
  25. package/lib/adapters/sessions.d.ts +97 -1
  26. package/lib/adapters/sessions.js +455 -15
  27. package/lib/adapters/settings.d.ts +3 -3
  28. package/lib/adapters/settings.js +23 -26
  29. package/lib/adapters/skills.d.ts +3 -3
  30. package/lib/adapters/skills.js +5 -3
  31. package/lib/adapters/test-helpers.d.ts +8 -0
  32. package/lib/adapters/test-helpers.js +8 -0
  33. package/lib/adapters/ui.d.ts +3 -3
  34. package/lib/adapters/ui.js +14 -17
  35. package/lib/adapters/workspaces.d.ts +22 -0
  36. package/lib/adapters/workspaces.js +237 -16
  37. package/lib/cli/index.d.ts +35 -4
  38. package/lib/cli/index.js +147 -25
  39. package/lib/cli/sessions-repair.d.ts +28 -0
  40. package/lib/cli/sessions-repair.js +206 -0
  41. package/lib/client.d.ts +260 -19
  42. package/lib/client.js +9257 -5984
  43. package/lib/core/analyzer.d.ts +67 -5
  44. package/lib/core/analyzer.js +402 -91
  45. package/lib/core/backup-plan.d.ts +7 -1
  46. package/lib/core/backup-plan.js +15 -8
  47. package/lib/core/backup.d.ts +53 -2
  48. package/lib/core/backup.js +120 -24
  49. package/lib/core/boot-safety.d.ts +50 -0
  50. package/lib/core/boot-safety.js +211 -0
  51. package/lib/core/config-lifecycle.d.ts +0 -1
  52. package/lib/core/config-lifecycle.js +0 -2
  53. package/lib/core/config-snapshot.d.ts +3 -1
  54. package/lib/core/config-snapshot.js +25 -6
  55. package/lib/core/credential-status.d.ts +15 -0
  56. package/lib/core/credential-status.js +13 -0
  57. package/lib/core/exporter.d.ts +18 -0
  58. package/lib/core/exporter.js +115 -3
  59. package/lib/core/importer.d.ts +15 -2
  60. package/lib/core/importer.js +10 -3
  61. package/lib/core/index.d.ts +2 -0
  62. package/lib/core/index.js +3 -0
  63. package/lib/core/journal.d.ts +74 -5
  64. package/lib/core/journal.js +127 -12
  65. package/lib/core/messages.d.ts +54 -0
  66. package/lib/core/messages.js +110 -0
  67. package/lib/core/migration-consult.js +0 -4
  68. package/lib/core/model-tools.js +6 -9
  69. package/lib/core/path-mapping.d.ts +11 -0
  70. package/lib/core/path-mapping.js +28 -0
  71. package/lib/core/phase3-host.d.ts +79 -6
  72. package/lib/core/phase3-host.js +223 -73
  73. package/lib/core/phase4-crash-child.js +0 -1
  74. package/lib/core/plugin-cli.js +1 -1
  75. package/lib/core/reconcile.d.ts +8 -1
  76. package/lib/core/reconcile.js +12 -4
  77. package/lib/core/recovery-orchestrator.js +1 -1
  78. package/lib/core/restore.js +10 -4
  79. package/lib/core/rollback.js +1 -1
  80. package/lib/core/run-registry.d.ts +63 -3
  81. package/lib/core/run-registry.js +74 -4
  82. package/lib/core/session-meta.d.ts +27 -8
  83. package/lib/core/session-meta.js +59 -32
  84. package/lib/core/session-repair.d.ts +89 -0
  85. package/lib/core/session-repair.js +118 -0
  86. package/lib/core/session-select.d.ts +50 -0
  87. package/lib/core/session-select.js +137 -0
  88. package/lib/core/startup-barrier.d.ts +15 -0
  89. package/lib/core/startup-barrier.js +24 -2
  90. package/lib/core/transaction-coordinator.js +3 -3
  91. package/lib/core/types.d.ts +241 -0
  92. package/lib/core/validator.js +0 -1
  93. package/lib/index.d.ts +367 -6
  94. package/lib/index.js +909 -3278
  95. package/lib/market/index-parser.d.ts +2 -1
  96. package/lib/market/index-parser.js +27 -6
  97. package/lib/market/my-repo.d.ts +0 -1
  98. package/lib/market/my-repo.js +0 -6
  99. package/lib/market/prepare.d.ts +9 -0
  100. package/lib/market/prepare.js +5 -31
  101. package/lib/market/reader.js +0 -1
  102. package/lib/market/security.js +3 -9
  103. package/lib/routes/backup.d.ts +9 -0
  104. package/lib/routes/backup.js +99 -0
  105. package/lib/routes/consult.d.ts +9 -0
  106. package/lib/routes/consult.js +134 -0
  107. package/lib/routes/context.d.ts +9 -0
  108. package/lib/routes/context.js +2 -0
  109. package/lib/routes/history.d.ts +9 -0
  110. package/lib/routes/history.js +53 -0
  111. package/lib/routes/import.d.ts +9 -0
  112. package/lib/routes/import.js +390 -0
  113. package/lib/routes/index.d.ts +10 -0
  114. package/lib/routes/index.js +27 -0
  115. package/lib/routes/kit.d.ts +98 -0
  116. package/lib/routes/kit.js +161 -0
  117. package/lib/routes/market.d.ts +9 -0
  118. package/lib/routes/market.js +256 -0
  119. package/lib/routes/me.d.ts +9 -0
  120. package/lib/routes/me.js +170 -0
  121. package/lib/routes/prefs.d.ts +9 -0
  122. package/lib/routes/prefs.js +103 -0
  123. package/lib/routes/profiles.d.ts +9 -0
  124. package/lib/routes/profiles.js +130 -0
  125. package/lib/routes/recovery.d.ts +9 -0
  126. package/lib/routes/recovery.js +137 -0
  127. package/lib/routes/snapshots.d.ts +9 -0
  128. package/lib/routes/snapshots.js +214 -0
  129. package/lib/routes/sync.d.ts +9 -0
  130. package/lib/routes/sync.js +783 -0
  131. package/lib/schema/config.d.ts +10 -9
  132. package/lib/schema/config.js +78 -96
  133. package/lib/schema/index.d.ts +8 -1
  134. package/lib/schema/index.js +13 -1
  135. package/lib/schema/manifest.d.ts +2 -0
  136. package/lib/schema/manifest.js +1 -0
  137. package/lib/schema/section-registry.d.ts +189 -0
  138. package/lib/schema/section-registry.js +258 -0
  139. package/lib/schema/types.d.ts +13 -3
  140. package/lib/schema/versions.d.ts +11 -6
  141. package/lib/schema/versions.js +27 -1
  142. package/lib/security/secret-scanner.js +1 -1
  143. package/lib/security/zip-security.d.ts +8 -5
  144. package/lib/security/zip-security.js +20 -90
  145. package/lib/sync/ancestor.d.ts +11 -0
  146. package/lib/sync/ancestor.js +20 -0
  147. package/lib/sync/autosync-config.js +9 -18
  148. package/lib/sync/autosync-scheduler.d.ts +7 -2
  149. package/lib/sync/autosync-scheduler.js +35 -17
  150. package/lib/sync/backup-schedule-config.js +0 -1
  151. package/lib/sync/blob-store.d.ts +85 -0
  152. package/lib/sync/blob-store.js +85 -0
  153. package/lib/sync/git/git-transport.d.ts +28 -5
  154. package/lib/sync/git/git-transport.js +170 -24
  155. package/lib/sync/github-auth.d.ts +0 -1
  156. package/lib/sync/github-auth.js +0 -2
  157. package/lib/sync/layout.d.ts +16 -2
  158. package/lib/sync/layout.js +48 -7
  159. package/lib/sync/risk.d.ts +18 -12
  160. package/lib/sync/risk.js +17 -28
  161. package/lib/sync/session-tombstones.d.ts +39 -0
  162. package/lib/sync/session-tombstones.js +125 -0
  163. package/lib/sync/sync-config.d.ts +26 -3
  164. package/lib/sync/sync-config.js +40 -3
  165. package/lib/sync/sync-engine.d.ts +140 -12
  166. package/lib/sync/sync-engine.js +266 -49
  167. package/lib/sync/sync-history.js +0 -1
  168. package/lib/sync/sync-selection.d.ts +18 -0
  169. package/lib/sync/sync-selection.js +50 -24
  170. package/lib/sync/sync-state.d.ts +31 -4
  171. package/lib/sync/sync-state.js +38 -6
  172. package/lib/sync/transport.d.ts +103 -0
  173. package/lib/sync/transport.js +121 -0
  174. package/lib/sync/ui-prefs.d.ts +4 -2
  175. package/lib/sync/ui-prefs.js +6 -4
  176. package/lib/sync/webdav/webdav-transport.d.ts +36 -4
  177. package/lib/sync/webdav/webdav-transport.js +242 -43
  178. package/lib/ui/export-flow.d.ts +2 -2
  179. package/lib/ui/export-flow.js +63 -17
  180. package/lib/ui/i18n.d.ts +12 -1
  181. package/lib/ui/i18n.js +25 -2
  182. package/lib/ui/import-log.d.ts +72 -0
  183. package/lib/ui/import-log.js +110 -0
  184. package/lib/ui/import-stepper.d.ts +14 -0
  185. package/lib/ui/import-stepper.js +18 -0
  186. package/lib/ui/import-wizard.d.ts +76 -0
  187. package/lib/ui/import-wizard.js +96 -2
  188. package/lib/ui/mid-ellipsis.d.ts +31 -0
  189. package/lib/ui/mid-ellipsis.js +39 -0
  190. package/lib/ui/my-configs-view.d.ts +136 -0
  191. package/lib/ui/my-configs-view.js +127 -0
  192. package/lib/ui/progress.js +6 -0
  193. package/lib/ui/report.d.ts +26 -0
  194. package/lib/ui/report.js +37 -0
  195. package/lib/ui/runs-view.d.ts +128 -0
  196. package/lib/ui/runs-view.js +123 -0
  197. package/lib/ui/selection-model.d.ts +90 -1
  198. package/lib/ui/selection-model.js +243 -1
  199. package/lib/ui/snapshots-view.d.ts +116 -0
  200. package/lib/ui/snapshots-view.js +141 -0
  201. package/lib/ui/sync-settings-view.d.ts +295 -0
  202. package/lib/ui/sync-settings-view.js +280 -0
  203. package/lib/ui/test-helpers.d.ts +7 -1
  204. package/lib/ui/test-helpers.js +4 -1
  205. package/lib/ui/types.d.ts +8 -1
  206. package/lib/utils/env-lock.d.ts +38 -5
  207. package/lib/utils/env-lock.js +55 -37
  208. package/lib/utils/logger.d.ts +17 -13
  209. package/lib/utils/logger.js +79 -41
  210. package/lib/utils/paths.d.ts +1 -1
  211. package/lib/utils/paths.js +2 -2
  212. package/lib/utils/session-log.d.ts +70 -0
  213. package/lib/utils/session-log.js +347 -0
  214. package/lib/utils/zip.d.ts +49 -5
  215. package/lib/utils/zip.js +240 -51
  216. package/lib/utils/zstd-frame.d.ts +44 -0
  217. package/lib/utils/zstd-frame.js +115 -0
  218. package/package.json +2 -2
  219. package/src/adapters/agent-instructions.ts +5 -3
  220. package/src/adapters/agent-presets.ts +5 -3
  221. package/src/adapters/credentials.ts +19 -22
  222. package/src/adapters/file-collection.test.ts +256 -0
  223. package/src/adapters/file-collection.ts +129 -17
  224. package/src/adapters/index.ts +12 -1
  225. package/src/adapters/json-section.ts +73 -0
  226. package/src/adapters/mcp.ts +16 -19
  227. package/src/adapters/plugin-files.ts +11 -14
  228. package/src/adapters/plugins.test.ts +1 -1
  229. package/src/adapters/plugins.ts +39 -42
  230. package/src/adapters/prompts.ts +16 -20
  231. package/src/adapters/providers.ts +22 -25
  232. package/src/adapters/roundtrip.test.ts +44 -0
  233. package/src/adapters/sessions.test.ts +400 -1
  234. package/src/adapters/sessions.ts +435 -16
  235. package/src/adapters/settings.ts +22 -25
  236. package/src/adapters/skills.ts +5 -3
  237. package/src/adapters/test-helpers.ts +8 -0
  238. package/src/adapters/ui.ts +14 -17
  239. package/src/adapters/workspaces.test.ts +226 -2
  240. package/src/adapters/workspaces.ts +247 -16
  241. package/src/cli/index.ts +154 -28
  242. package/src/cli/sessions-repair.ts +229 -0
  243. package/src/client/CLIENT_DEPENDENCIES.md +3 -1
  244. package/src/client/ConfigManagerSection.tsx +38 -7
  245. package/src/client/api.ts +160 -237
  246. package/src/client/common/ContentPicker.tsx +33 -8
  247. package/src/client/common/ProgressBar.tsx +18 -9
  248. package/src/client/common/ReportView.tsx +127 -53
  249. package/src/client/common/RunsCenter.tsx +377 -0
  250. package/src/client/common/client-route-parity.test.ts +171 -0
  251. package/src/client/common/http-usage-guard.test.ts +163 -0
  252. package/src/client/common/http.test.ts +225 -0
  253. package/src/client/common/http.ts +238 -0
  254. package/src/client/common/plan-text-redaction.test.ts +29 -7
  255. package/src/client/common/progress-view.test.ts +33 -1
  256. package/src/client/common/progress-view.ts +17 -0
  257. package/src/client/common/routes.ts +132 -0
  258. package/src/client/common/runs-center-guard.test.ts +44 -0
  259. package/src/client/config-manager.module.css +332 -1
  260. package/src/client/export/ExportView.tsx +27 -3
  261. package/src/client/history/HistoryPanel.tsx +0 -1
  262. package/src/client/history/history-api.ts +34 -61
  263. package/src/client/import/ImportLogPanel.tsx +136 -0
  264. package/src/client/import/ImportWizardView.tsx +442 -836
  265. package/src/client/import/PathMappingForm.tsx +12 -1
  266. package/src/client/import/import-wizard-split.test.ts +101 -0
  267. package/src/client/import/import-wizard-steps.tsx +352 -0
  268. package/src/client/import/use-import-wizard-controller.ts +459 -0
  269. package/src/client/index.ts +0 -2
  270. package/src/client/lifecycle/lifecycle-api.ts +17 -88
  271. package/src/client/locales.ts +140 -0
  272. package/src/client/market/MyConfigsInstall.tsx +92 -0
  273. package/src/client/market/MyConfigsList.tsx +120 -0
  274. package/src/client/market/MyConfigsLoginCard.tsx +99 -0
  275. package/src/client/market/MyConfigsView.tsx +148 -476
  276. package/src/client/market/MyConfigsWizard.tsx +252 -0
  277. package/src/client/market/market-api.test.ts +36 -0
  278. package/src/client/market/market-api.ts +13 -70
  279. package/src/client/market/market-view.ts +1 -1
  280. package/src/client/market/my-configs-api.ts +17 -71
  281. package/src/client/market/my-configs-split.test.ts +58 -0
  282. package/src/client/overview/OverviewPanel.tsx +1 -9
  283. package/src/client/recovery/RecoveryPanel.tsx +1 -2
  284. package/src/client/recovery/recovery-api.ts +15 -65
  285. package/src/client/run-store.test.ts +421 -3
  286. package/src/client/run-store.ts +284 -53
  287. package/src/client/snapshots/BackupScheduleCard.tsx +437 -0
  288. package/src/client/snapshots/SnapshotsEmptyState.tsx +31 -0
  289. package/src/client/snapshots/SnapshotsListTable.tsx +97 -0
  290. package/src/client/snapshots/SnapshotsPanel.tsx +42 -626
  291. package/src/client/sync/AutosyncCard.tsx +88 -0
  292. package/src/client/sync/ChannelConfigDialog.tsx +268 -0
  293. package/src/client/sync/DecryptPasswordCard.tsx +48 -0
  294. package/src/client/sync/SecurityOptionsCard.tsx +83 -0
  295. package/src/client/sync/SyncChannelEntryCard.tsx +78 -0
  296. package/src/client/sync/SyncConfirmView.tsx +10 -8
  297. package/src/client/sync/SyncSectionPickerDialog.tsx +182 -0
  298. package/src/client/sync/SyncSettingsView.test.ts +85 -0
  299. package/src/client/sync/SyncSettingsView.tsx +230 -649
  300. package/src/client/sync/history-model.ts +4 -10
  301. package/src/client/sync/sync-api.ts +59 -100
  302. package/src/client/sync/sync-locales.ts +16 -2
  303. package/src/client/sync/sync-view-v2.test.ts +30 -4
  304. package/src/client/sync/sync-view.ts +67 -17
  305. package/src/core/analyzer-mapping.test.ts +55 -0
  306. package/src/core/analyzer.ts +451 -91
  307. package/src/core/apply-order.test.ts +182 -0
  308. package/src/core/backup-plan.test.ts +141 -0
  309. package/src/core/backup-plan.ts +16 -8
  310. package/src/core/backup.test.ts +310 -0
  311. package/src/core/backup.ts +174 -23
  312. package/src/core/base-path-rebase.test.ts +97 -0
  313. package/src/core/boot-rescue.test.ts +26 -0
  314. package/src/core/boot-safety.test.ts +138 -0
  315. package/src/core/boot-safety.ts +248 -0
  316. package/src/core/config-lifecycle.test.ts +255 -2
  317. package/src/core/config-lifecycle.ts +0 -2
  318. package/src/core/config-snapshot.test.ts +193 -3
  319. package/src/core/config-snapshot.ts +24 -8
  320. package/src/core/credential-status.test.ts +35 -0
  321. package/src/core/credential-status.ts +25 -0
  322. package/src/core/export-coupling.test.ts +255 -0
  323. package/src/core/exporter.ts +110 -3
  324. package/src/core/finalize-order.test.ts +82 -0
  325. package/src/core/import-sessions-visibility.test.ts +93 -0
  326. package/src/core/importer.ts +21 -3
  327. package/src/core/index.ts +10 -0
  328. package/src/core/journal.test.ts +59 -2
  329. package/src/core/journal.ts +152 -10
  330. package/src/core/messages.ts +110 -0
  331. package/src/core/migration-consult.ts +0 -5
  332. package/src/core/migration-history.test.ts +3 -5
  333. package/src/core/model-tools.test.ts +0 -14
  334. package/src/core/model-tools.ts +6 -8
  335. package/src/core/path-mapping.ts +33 -0
  336. package/src/core/phase1-wiring.test.ts +12 -3
  337. package/src/core/phase3-consistency.test.ts +1 -1
  338. package/src/core/phase3-host.ts +248 -70
  339. package/src/core/phase3-p2b-fingerprint.test.ts +0 -1
  340. package/src/core/phase3-production-integration.test.ts +1 -1
  341. package/src/core/phase4-crash-child.ts +0 -1
  342. package/src/core/phase4-crash-injection.test.ts +1 -3
  343. package/src/core/plugin-cli.fs.test.ts +2 -2
  344. package/src/core/plugin-cli.ts +1 -1
  345. package/src/core/reconcile.test.ts +62 -0
  346. package/src/core/reconcile.ts +20 -5
  347. package/src/core/recovery-orchestrator.test.ts +13 -1
  348. package/src/core/recovery-orchestrator.ts +1 -1
  349. package/src/core/restore.ts +10 -4
  350. package/src/core/rollback.ts +1 -1
  351. package/src/core/run-registry-runs.test.ts +71 -0
  352. package/src/core/run-registry.test.ts +58 -7
  353. package/src/core/run-registry.ts +96 -5
  354. package/src/core/session-meta.test.ts +59 -1
  355. package/src/core/session-meta.ts +62 -29
  356. package/src/core/session-repair.test.ts +91 -0
  357. package/src/core/session-repair.ts +203 -0
  358. package/src/core/session-select.test.ts +73 -0
  359. package/src/core/session-select.ts +131 -0
  360. package/src/core/smoke.test.ts +1 -2
  361. package/src/core/startup-barrier.ts +29 -2
  362. package/src/core/transaction-coordinator.ts +3 -3
  363. package/src/core/types.ts +244 -0
  364. package/src/core/validator.ts +0 -1
  365. package/src/index.sync.test.ts +8 -0
  366. package/src/index.ts +530 -2757
  367. package/src/market/git-file-writer.test.ts +1 -1
  368. package/src/market/index-parser.ts +29 -6
  369. package/src/market/market.test.ts +146 -1
  370. package/src/market/my-repo.test.ts +57 -4
  371. package/src/market/my-repo.ts +0 -7
  372. package/src/market/prepare.ts +12 -32
  373. package/src/market/reader.ts +0 -1
  374. package/src/market/security.ts +3 -11
  375. package/src/routes/backup.ts +103 -0
  376. package/src/routes/consult.ts +139 -0
  377. package/src/routes/context.ts +10 -0
  378. package/src/routes/history.ts +56 -0
  379. package/src/routes/import.ts +399 -0
  380. package/src/routes/index.ts +36 -0
  381. package/src/routes/kit.ts +222 -0
  382. package/src/routes/market.ts +275 -0
  383. package/src/routes/me.ts +168 -0
  384. package/src/routes/prefs.ts +104 -0
  385. package/src/routes/profiles.ts +131 -0
  386. package/src/routes/recovery.ts +113 -0
  387. package/src/routes/snapshots.ts +219 -0
  388. package/src/routes/sync.ts +810 -0
  389. package/src/schema/config.ts +96 -91
  390. package/src/schema/index.ts +33 -1
  391. package/src/schema/manifest.ts +3 -0
  392. package/src/schema/registry.test.ts +257 -0
  393. package/src/schema/section-registry.ts +423 -0
  394. package/src/schema/types.ts +13 -3
  395. package/src/schema/versions.ts +31 -1
  396. package/src/security/secret-scanner.ts +1 -1
  397. package/src/security/security.test.ts +145 -2
  398. package/src/security/zip-security.ts +20 -92
  399. package/src/sync/ancestor.test.ts +0 -1
  400. package/src/sync/ancestor.ts +23 -0
  401. package/src/sync/autosync-config.ts +9 -18
  402. package/src/sync/autosync-scheduler.test.ts +60 -3
  403. package/src/sync/autosync-scheduler.ts +45 -19
  404. package/src/sync/backup-schedule-config.ts +0 -1
  405. package/src/sync/backup-scheduler.test.ts +15 -2
  406. package/src/sync/blob-store.test.ts +191 -0
  407. package/src/sync/blob-store.ts +139 -0
  408. package/src/sync/git/git-transport.test.ts +203 -1
  409. package/src/sync/git/git-transport.ts +173 -24
  410. package/src/sync/github-auth.ts +0 -2
  411. package/src/sync/layout.ts +62 -8
  412. package/src/sync/risk.test.ts +32 -1
  413. package/src/sync/risk.ts +26 -31
  414. package/src/sync/session-tombstones.test.ts +87 -0
  415. package/src/sync/session-tombstones.ts +123 -0
  416. package/src/sync/sync-config.test.ts +132 -0
  417. package/src/sync/sync-config.ts +47 -6
  418. package/src/sync/sync-credentials.test.ts +77 -0
  419. package/src/sync/sync-engine.test.ts +799 -8
  420. package/src/sync/sync-engine.ts +322 -51
  421. package/src/sync/sync-history.ts +0 -1
  422. package/src/sync/sync-selection.test.ts +43 -7
  423. package/src/sync/sync-selection.ts +56 -24
  424. package/src/sync/sync-state.test.ts +83 -0
  425. package/src/sync/sync-state.ts +58 -7
  426. package/src/sync/transport.ts +191 -0
  427. package/src/sync/ui-prefs.ts +10 -6
  428. package/src/sync/webdav/webdav-transport.test.ts +122 -2
  429. package/src/sync/webdav/webdav-transport.ts +256 -37
  430. package/src/ui/export-flow.ts +65 -18
  431. package/src/ui/i18n.test.ts +149 -0
  432. package/src/ui/i18n.ts +25 -2
  433. package/src/ui/import-log.test.ts +106 -0
  434. package/src/ui/import-log.ts +158 -0
  435. package/src/ui/import-stepper.test.ts +25 -1
  436. package/src/ui/import-stepper.ts +18 -0
  437. package/src/ui/import-wizard.test.ts +130 -2
  438. package/src/ui/import-wizard.ts +135 -2
  439. package/src/ui/market-import.test.ts +0 -1
  440. package/src/ui/mid-ellipsis.test.ts +160 -0
  441. package/src/ui/mid-ellipsis.ts +37 -0
  442. package/src/ui/my-configs-view.test.ts +222 -0
  443. package/src/ui/my-configs-view.ts +206 -0
  444. package/src/ui/progress.ts +6 -0
  445. package/src/ui/report.test.ts +32 -2
  446. package/src/ui/report.ts +52 -0
  447. package/src/ui/runs-view.test.ts +152 -0
  448. package/src/ui/runs-view.ts +207 -0
  449. package/src/ui/selection-model.test.ts +268 -7
  450. package/src/ui/selection-model.ts +277 -1
  451. package/src/ui/snapshots-view.test.ts +228 -0
  452. package/src/ui/snapshots-view.ts +233 -0
  453. package/src/ui/sync-settings-view.test.ts +348 -0
  454. package/src/ui/sync-settings-view.ts +516 -0
  455. package/src/ui/test-helpers.ts +8 -1
  456. package/src/ui/types.ts +10 -1
  457. package/src/utils/env-lock.test.ts +170 -107
  458. package/src/utils/env-lock.ts +71 -35
  459. package/src/utils/logger.test.ts +178 -1
  460. package/src/utils/logger.ts +81 -44
  461. package/src/utils/paths.ts +2 -2
  462. package/src/utils/session-log.test.ts +136 -0
  463. package/src/utils/session-log.ts +344 -0
  464. package/src/utils/zip.test.ts +284 -0
  465. package/src/utils/zip.ts +226 -52
  466. package/src/utils/zstd-frame.test.ts +80 -0
  467. package/src/utils/zstd-frame.ts +131 -0
@@ -0,0 +1,256 @@
1
+ /**
2
+ * 文件类分区导出的**字节闸门**(审计 core-flow F-12 / sync#F-12)测试。
3
+ *
4
+ * 覆盖:
5
+ * ① 超限 → 单元**整块**剔除,且被剔除的单元写进 warnings(报告可见,绝不静默);
6
+ * ② 未超限 → 与「改造前」的收集逻辑**逐项一致**(自包含对照:legacy 循环逐字内联,不依赖 git 回退);
7
+ * ③ 单元不可拆分:只放得下一半时一个文件都不留(宁可少带,不可半带);
8
+ * ④ 单元成员在遍历结果里**不连续**也不会被劈开(分组在闸门之前完成);
9
+ * ⑤ 放不下的单元跳过后**继续尝试后面的单元**(后面的小单元仍可能入选);
10
+ * ⑥ 告警列表有上限(大分区不刷屏),被折叠的数量的确进了文案。
11
+ * ⑦ validate 的样板收敛:object 守卫并入共享骨架 validateJsonSection(走可选 objectMessageKey)后,
12
+ * 5 个文件类分区的用户可见报错文案与改造前逐字相同(zh / en / 缺省三条路径)。
13
+ *
14
+ * 阈值:测试用探针把闸门降到几十~几百字节 —— 真实阈值 MAX_FILE_SECTION_BYTES = 256 MiB
15
+ * 无法在单测里构造而不吃掉 256 MiB 内存(这正是闸门要防的东西)。
16
+ */
17
+ import test from 'node:test';
18
+ import assert from 'node:assert/strict';
19
+
20
+ import { FileCollectionAdapter, MAX_FILE_SECTION_BYTES } from './file-collection.ts';
21
+ import { SkillsAdapter } from './skills.ts';
22
+ import { listFilesDetailed } from './link-report.ts';
23
+ import { makeContext, sha256Hex } from './test-helpers.ts';
24
+ import { AgentPresetsAdapter } from './agent-presets.ts';
25
+ import { AgentInstructionsAdapter } from './agent-instructions.ts';
26
+ import { SessionsAdapter } from './sessions.ts';
27
+ import { SelfAdapter } from './self.ts';
28
+ import { makeMsg, zhMsg } from '../core/messages.ts';
29
+ import { validateJsonSection } from './json-section.ts';
30
+ import type { FileEntry } from '../schema/types.ts';
31
+ import type { MockHostContext } from './test-helpers.ts';
32
+
33
+ /** 探针:与 skills 同构,但把闸门降到 `limit` 字节(生产代码不覆写 sectionByteLimit) */
34
+ class ProbeAdapter extends FileCollectionAdapter {
35
+ readonly id = 'skills' as const;
36
+ readonly displayName = 'Skills';
37
+ readonly defaultIncluded = true;
38
+ readonly portability = 'portable' as const;
39
+ readonly baseDir = 'skills';
40
+ private readonly limit: number;
41
+ constructor(limit: number) {
42
+ super();
43
+ this.limit = limit;
44
+ }
45
+ protected override sectionByteLimit(): number {
46
+ return this.limit;
47
+ }
48
+ }
49
+
50
+ /** 写入 n 字节的零内容文件(内容无所谓,闸门只看 byteLength) */
51
+ async function writeSized(ctx: MockHostContext, rel: string, n: number): Promise<void> {
52
+ await ctx.fs.writeFile(rel, new Uint8Array(n));
53
+ }
54
+
55
+ function paths(section: { data: { files: { relativePath: string }[] } }): string[] {
56
+ return section.data.files.map((f) => f.relativePath);
57
+ }
58
+
59
+ /* ---------------- ② 未超限:与改造前逐项一致(自包含对照) ---------------- */
60
+
61
+ /**
62
+ * 「改造前」的收集循环(逐字复刻 Gate 落地前的实现):
63
+ * listFilesDetailed → relPathOf → unitIdOf 白名单 → readFile → push。
64
+ * 用它当参照,验证闸门在**未触发**时不改变任何一项(顺序 / 路径 / 字节 / hash)。
65
+ */
66
+ async function legacyCollect(ctx: MockHostContext, baseDir: string, allow: readonly string[] | undefined): Promise<FileEntry[]> {
67
+ const listing = await listFilesDetailed(ctx.fs, baseDir);
68
+ const out: FileEntry[] = [];
69
+ for (const rel of listing.paths) {
70
+ const relPath = rel.replace(new RegExp('^' + baseDir + '[\\\\/]'), '');
71
+ const unit = relPath.includes('/') ? relPath.slice(0, relPath.indexOf('/')) : relPath;
72
+ if (allow !== undefined && !allow.includes('skills:' + unit)) continue;
73
+ const data = await ctx.fs.readFile(rel);
74
+ out.push({ relativePath: relPath, data, contentHash: sha256Hex(data) });
75
+ }
76
+ return out;
77
+ }
78
+
79
+ test('字节闸门:未超限时与改造前的收集逻辑逐项一致(顺序/路径/字节/hash + 零告警)', async () => {
80
+ const ctx = makeContext('win32', 'C:\\Users\\alice');
81
+ await writeSized(ctx, 'skills/alpha/a.md', 100);
82
+ await writeSized(ctx, 'skills/alpha/b.md', 50);
83
+ await writeSized(ctx, 'skills/beta/c.md', 70);
84
+ await writeSized(ctx, 'skills/loose.md', 10);
85
+
86
+ // 闸门远大于总字节(190)→ 一次都不该触发
87
+ const section = await new ProbeAdapter(10_000).export(ctx, { includeSecrets: false });
88
+ const legacy = await legacyCollect(ctx, 'skills', undefined);
89
+
90
+ assert.deepEqual(section.warnings, [], '未超限不得产出任何告警');
91
+ assert.deepEqual(
92
+ section.data.files.map((f) => ({ relativePath: f.relativePath, bytes: f.data.byteLength, contentHash: f.contentHash })),
93
+ legacy.map((f) => ({ relativePath: f.relativePath, bytes: f.data.byteLength, contentHash: f.contentHash })),
94
+ '逐项一致:顺序 + 相对路径 + 字节数 + SHA-256',
95
+ );
96
+ assert.deepEqual(paths(section), ['alpha/a.md', 'alpha/b.md', 'beta/c.md', 'loose.md']);
97
+ assert.equal(section.counts.files, 4);
98
+
99
+ // 白名单路径同样逐项一致(闸门不改变 includeItems 语义)
100
+ const allow = ['skills:alpha'];
101
+ const filtered = await new ProbeAdapter(10_000).export(ctx, { includeSecrets: false, includeItems: { skills: allow } });
102
+ const filteredLegacy = await legacyCollect(ctx, 'skills', allow);
103
+ assert.deepEqual(paths(filtered), filteredLegacy.map((f) => f.relativePath));
104
+ assert.deepEqual(filtered.warnings, []);
105
+ });
106
+
107
+ /* ---------------- ① 超限:整块剔除 + 告警可见 ---------------- */
108
+
109
+ test('字节闸门:超限时按单元整块剔除,并把该单元写进告警(绝不静默)', async () => {
110
+ const ctx = makeContext('win32', 'C:\\Users\\alice');
111
+ await writeSized(ctx, 'skills/alpha/a.md', 120);
112
+ await writeSized(ctx, 'skills/alpha/b.md', 80); // alpha = 200
113
+ await writeSized(ctx, 'skills/beta/c.md', 200); // beta = 200,room 只剩 100 → 放不下
114
+
115
+ const section = await new ProbeAdapter(300).export(ctx, { includeSecrets: false });
116
+
117
+ assert.deepEqual(paths(section), ['alpha/a.md', 'alpha/b.md'], 'beta 整块不进备份');
118
+ assert.equal(section.counts.files, 2, 'counts 反映实际带走的文件数');
119
+ const gate = section.warnings.filter((w) => w.includes('单分区上限'));
120
+ assert.equal(gate.length, 1, `应恰有一条闸门告警,实际: ${section.warnings.join(' | ')}`);
121
+ const line = gate[0] ?? '';
122
+ assert.match(line, /1 个单元/, '告警给出被剔除的单元数');
123
+ assert.match(line, /300 B/, '告警给出上限值');
124
+ assert.match(line, /200\.0 B|200 B/, '告警给出实际保留量');
125
+ assert.match(line, /—— beta$/, '告警列出被剔除的单元 id(无折叠后缀)');
126
+
127
+ // 内容确实不在产物里(不是「报了警但其实还在」)
128
+ assert.equal(section.data.files.some((f) => f.relativePath.startsWith('beta/')), false);
129
+ });
130
+
131
+ test('字节闸门:单元不可拆分 —— 只放得下一半时,该单元一个文件都不留', async () => {
132
+ const ctx = makeContext('win32', 'C:\\Users\\alice');
133
+ await writeSized(ctx, 'skills/skill-a/one.md', 60);
134
+ await writeSized(ctx, 'skills/skill-a/two.md', 60); // 单元合计 120 > 上限 100
135
+
136
+ const section = await new ProbeAdapter(100).export(ctx, { includeSecrets: false });
137
+
138
+ assert.deepEqual(section.data.files, [], '整块剔除:不能只留 60 字节的那一半');
139
+ assert.equal(section.counts.files, 0);
140
+ assert.match(section.warnings.find((w) => w.includes('单分区上限')) ?? '', /—— skill-a$/);
141
+ });
142
+
143
+ /* ---------------- ④ 成员不连续也不会被劈开 ---------------- */
144
+
145
+ test('字节闸门:单元的成员在遍历结果里不连续时同样整块判定(不会被劈成两半)', async () => {
146
+ const ctx = makeContext('win32', 'C:\\Users\\alice');
147
+ await ctx.fs.writeFile('skills/a/1.md', new Uint8Array(60));
148
+ await ctx.fs.writeFile('skills/b/1.md', new Uint8Array(60));
149
+ await ctx.fs.writeFile('skills/a/2.md', new Uint8Array(60));
150
+ // 故意让遍历结果按 a、b、a 交错返回(真实 walk 不保证顺序,mock 直接钉住这条边界)
151
+ ctx.fs.listRecursiveDetailed = async () => ({
152
+ paths: ['skills/a/1.md', 'skills/b/1.md', 'skills/a/2.md'],
153
+ skippedLinks: [],
154
+ followedLinks: 0,
155
+ unreadableDirs: [],
156
+ });
157
+
158
+ const section = await new ProbeAdapter(150).export(ctx, { includeSecrets: false });
159
+
160
+ // a = 120(两段合计)→ 入选;b = 60(room 只剩 30)→ 整块剔除
161
+ assert.deepEqual(paths(section), ['a/1.md', 'a/2.md'], 'a 的两段一起入选(未按段拆开),b 整体剔除');
162
+ assert.match(section.warnings.find((w) => w.includes('单分区上限')) ?? '', /—— b$/);
163
+ });
164
+
165
+ /* ---------------- ⑤ 跳过后继续尝试后面的单元 ---------------- */
166
+
167
+ test('字节闸门:一个巨型单元被剔除后,后面的小单元仍可能入选(尽量多带)', async () => {
168
+ const ctx = makeContext('win32', 'C:\\Users\\alice');
169
+ // 遍历顺序是字典序:big < huge < small —— 前 200 入选,huge(200)放不下被整块剔除,
170
+ // 后面 room 仍是 100 的 small 不受牵连(硬停会把 small 一起丢掉)。
171
+ await writeSized(ctx, 'skills/big/a.md', 200);
172
+ await writeSized(ctx, 'skills/huge/a.md', 200);
173
+ await writeSized(ctx, 'skills/small/a.md', 50);
174
+
175
+ const section = await new ProbeAdapter(300).export(ctx, { includeSecrets: false });
176
+
177
+ assert.deepEqual(paths(section), ['big/a.md', 'small/a.md'], '一个巨型单元不该把它之后的内容一起带走');
178
+ assert.match(section.warnings.find((w) => w.includes('单分区上限')) ?? '', /—— huge$/);
179
+ });
180
+
181
+ /* ---------------- ⑥ 告警列表有上限,折叠数量不丢 ---------------- */
182
+
183
+ test('字节闸门:被剔除单元很多时告警只列前 5 个,剩余折叠为 (+ N)(不刷屏也不静默)', async () => {
184
+ const ctx = makeContext('win32', 'C:\\Users\\alice');
185
+ await writeSized(ctx, 'skills/first/a.md', 100);
186
+ for (let i = 0; i < 7; i += 1) await writeSized(ctx, `skills/u${i}/a.md`, 100); // 每个 100,room 只剩 0 → 全部剔除
187
+
188
+ const section = await new ProbeAdapter(100).export(ctx, { includeSecrets: false });
189
+
190
+ const line = section.warnings.find((w) => w.includes('单分区上限')) ?? '';
191
+ assert.deepEqual(paths(section), ['first/a.md']);
192
+ assert.match(line, /7 个单元/);
193
+ assert.match(line, /\(\+ 2\)/, '折叠数量必须出现在文案里(否则截断本身又成了静默丢失)');
194
+ for (const name of ['u0', 'u1', 'u2', 'u3', 'u4']) assert.match(line, new RegExp('\\b' + name + '\\b'));
195
+ assert.equal(line.includes('u5'), false, '第 6 个起折叠');
196
+ });
197
+
198
+ /* ---------------- 默认阈值 ---------------- */
199
+
200
+ test('字节闸门:默认阈值 = 256 MiB(与导入侧上传硬上限对齐),且普通内容不触发', async () => {
201
+ assert.equal(MAX_FILE_SECTION_BYTES, 256 * 1024 * 1024);
202
+ assert.equal(MAX_FILE_SECTION_BYTES, 268435456);
203
+
204
+ const ctx = makeContext('win32', 'C:\\Users\\alice');
205
+ await writeSized(ctx, 'skills/big/a.md', 2 * 1024 * 1024); // 2 MiB:远低于阈值
206
+ const section = await new SkillsAdapter().export(ctx, { includeSecrets: false });
207
+ assert.deepEqual(paths(section), ['big/a.md'], '真实适配器在默认阈值下不做任何剔除');
208
+ assert.deepEqual(section.warnings, []);
209
+ });
210
+
211
+ /* ---------------- ⑦ validate:object 守卫并入共享骨架后文案逐字不变 ---------------- */
212
+
213
+ test('validateJsonSection:objectMessageKey 可选,缺省即 adapter.validate.object,且不改变其余语义', () => {
214
+ // ① 不传第 6 参 = 既有 9 个调用点的行为(缺省键 + subject 插值)
215
+ assert.deepEqual(validateJsonSection<{ version?: unknown }>('skills', null, zhMsg), {
216
+ valid: false,
217
+ issues: [{ path: '$', message: 'skills 数据必须是对象', severity: 'error' }],
218
+ });
219
+ // ② 自定义键:文案取自该键;文件类分区的专用键不含 {subject},故第 5 参取值不影响输出
220
+ assert.deepEqual(validateJsonSection<{ version?: unknown }>('skills', null, zhMsg, undefined, 'skills', 'adapter.validate.fileSection'), {
221
+ valid: false,
222
+ issues: [{ path: '$', message: '文件分区数据必须是对象', severity: 'error' }],
223
+ });
224
+ assert.deepEqual(validateJsonSection<{ version?: unknown }>('skills', null, zhMsg, undefined, '别名', 'adapter.validate.fileSection'), {
225
+ valid: false,
226
+ issues: [{ path: '$', message: '文件分区数据必须是对象', severity: 'error' }],
227
+ });
228
+ // ③ en 目录同样走自定义键
229
+ assert.deepEqual(validateJsonSection<{ version?: unknown }>('skills', null, makeMsg('en'), undefined, 'skills', 'adapter.validate.fileSection'), {
230
+ valid: false,
231
+ issues: [{ path: '$', message: 'File-section data must be an object', severity: 'error' }],
232
+ });
233
+ // ④ object 守卫之外的语义与新增参数无关(version 不匹配仍继续做形状检查)
234
+ assert.deepEqual(validateJsonSection<{ version?: unknown }>('skills', { version: 2 }, zhMsg), {
235
+ valid: false,
236
+ issues: [{ path: 'version', message: 'version 必须为 1(收到 2)', severity: 'error' }],
237
+ });
238
+ });
239
+
240
+ test('validate:5 个文件类分区的 object 守卫并入骨架后,用户可见文案与改造前逐字相同', async () => {
241
+ const fileAdapters = [
242
+ new SkillsAdapter(), new AgentPresetsAdapter(), new AgentInstructionsAdapter(),
243
+ new SessionsAdapter(), new SelfAdapter(),
244
+ ];
245
+ const zhExpected = { valid: false, issues: [{ path: '$', message: '文件分区数据必须是对象', severity: 'error' }] };
246
+ const enExpected = { valid: false, issues: [{ path: '$', message: 'File-section data must be an object', severity: 'error' }] };
247
+ for (const adapter of fileAdapters) {
248
+ assert.deepEqual(await adapter.validate(null as never), zhExpected, adapter.id + ':缺省(zh)文案');
249
+ assert.deepEqual(await adapter.validate('x' as never, makeMsg('en')), enExpected, adapter.id + ':en 文案');
250
+ assert.deepEqual(await adapter.validate(42 as never, makeMsg('zh')), zhExpected, adapter.id + ':任何非对象都是同一条');
251
+ // 专用键不含 {subject}:并入骨架后若误用缺省键,文案会变成「<分区 id> 数据必须是对象」
252
+ const message = (await adapter.validate(null as never)).issues[0]?.message ?? '';
253
+ assert.equal(message.includes(adapter.id), false, adapter.id + ':文案不得退化成 <subject> 数据必须是对象');
254
+ }
255
+ });
256
+
@@ -6,6 +6,10 @@
6
6
  *
7
7
  * 文件内容以真实文件进入 ZIP(custom/skills/… 等前缀由 exporter 按 SECTION_FILE_PREFIXES 处理)。
8
8
  * 幂等:相对路径 + 内容 SHA-256 hash 比对(重复导入 → Skip)。
9
+ *
10
+ * 导出的**单分区字节闸门**(审计 core-flow F-12 / sync#F-12):累计到 MAX_FILE_SECTION_BYTES 后,
11
+ * 放不下的单元**整块**剔除并写进告警(见 export())。覆盖 skills / agentPresets / sessions;
12
+ * agentInstructions(单文件 AGENTS.md)与 self(白名单配置小文件)各自覆写 export,不在本闸门射程内。
9
13
  */
10
14
  import path from 'node:path';
11
15
  import { sha256Hex } from '../utils/hashing.ts';
@@ -20,8 +24,42 @@ import type {
20
24
  ImportContext, PlanItem, Portability, ValidationResult,
21
25
  } from '../core/types.ts';
22
26
  import { defaultUnitId, unitAllowed, unitsFromFiles } from './units.ts';
27
+ import { validateJsonSection } from './json-section.ts';
28
+
29
+ /**
30
+ * 文件类分区导出的**单分区累计字节闸门**:分区内文件内容合计放不下的单元整块剔除(见 export())。
31
+ *
32
+ * 取值依据(不是随手拍的数):
33
+ * 1) **对称性** —— 导入侧对上传的备份包有硬上限 256 MiB(MAX_UPLOAD_BYTES,src/index.ts 与
34
+ * src/routes/kit.ts 的上传路径);导出侧此前**没有任何体积约束**,于是会出现「导出成功、
35
+ * 传不回来」(审计 core-flow F-12 / sync#F-12)。取同一个 256 MiB 作**单分区**上限:
36
+ * 任何单个分区都落在导入侧上传路径能收的范围内。
37
+ * 2) **内存峰值** —— 导出时文件内容以完整 Uint8Array 驻留内存(ZIP 组装前全量在内存里),同步通道
38
+ * 还要再写一份。闸门把「单分区常驻」钳在 256 MiB 以内(峰值 ≈ 上限 + 正在读的那个单元),
39
+ * 不再是「会话附件多大就多吃多少」。
40
+ * 3) **与既有同类上限同量级** —— MAX_MARKET_ZIP_BYTES = 64 MiB、DEFAULT_MAX_JSON_BYTES = 64 MiB、
41
+ * MAX_LOCAL_TARBALL_BYTES = 100 MiB、MAX_PATCH_FILE_BYTES = 2 MiB。文件类分区装的是**用户真实
42
+ * 文件**,所以取「导入侧能收」的 256 MiB,而不是为省内存随意调小(那会平白少带用户数据)。
43
+ * 4) **为什么按分区而不是按整包** —— 整包体积由用户勾选跨分区决定;闸门要能给出「哪些分区、哪些
44
+ * 单元没进去」的可执行信息,落在分区/单元层才可解释。
45
+ *
46
+ * 超限时**绝不静默**:被整块剔除的单元清单进 ExportSection.warnings(导出报告可见)。
47
+ */
48
+ export const MAX_FILE_SECTION_BYTES = 256 * 1024 * 1024;
49
+
50
+ /** 字节闸门告警里最多列出的单元数(其余折叠成 "(+ N)",避免大分区把报告撑爆;与 link-report 同口径) */
51
+ const MAX_UNITS_PER_GATE_WARNING = 5;
52
+
53
+ /** 告警文案里的体积(人类可读;纯数值格式化,不含用户可见散文) */
54
+ function formatBytes(bytes: number): string {
55
+ if (bytes < 1024) return `${bytes} B`;
56
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`;
57
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
58
+ }
23
59
 
24
60
  export abstract class FileCollectionAdapter implements ConfigAdapter<FilesSection> {
61
+ /** 文件集合:`relativePath` 是身份而非配置 → 永不被导入时的前缀映射改写(见 ConfigAdapter.fileCollection) */
62
+ readonly fileCollection = true;
25
63
  abstract readonly id: SectionId;
26
64
  abstract readonly displayName: string;
27
65
  abstract readonly defaultIncluded: boolean;
@@ -47,6 +85,17 @@ export abstract class FileCollectionAdapter implements ConfigAdapter<FilesSectio
47
85
  return rel.replace(new RegExp('^' + prefix + '[\\\\/]'), '');
48
86
  }
49
87
 
88
+ /**
89
+ * 单分区累计字节上限(缺省 MAX_FILE_SECTION_BYTES = 256 MiB,见该常量注释)。
90
+ *
91
+ * 生产代码不覆写;留成受保护方法只为让测试能构造「超限」场景(真实阈值 256 MiB 无法在单测里
92
+ * 造出来而不吃掉 256 MiB 内存)。若要给用户配置入口,应走 ExportOptions(src/core/types.ts),
93
+ * 而不是在子类里飘一个隐式默认值。
94
+ */
95
+ protected sectionByteLimit(): number {
96
+ return MAX_FILE_SECTION_BYTES;
97
+ }
98
+
50
99
  /**
51
100
  * 条目级选择(Phase 1):只导出白名单命中的单元。
52
101
  *
@@ -85,13 +134,68 @@ export abstract class FileCollectionAdapter implements ConfigAdapter<FilesSectio
85
134
  const restricted = await this.restrictUnits(ctx, rels, options);
86
135
  if (restricted !== null) warnings.push(...restricted.warnings);
87
136
  const keep = restricted?.keep ?? null;
137
+ // ① 先按「单元」分组(零 I/O:只看路径)。单元的成员文件**不保证**在遍历结果里连续,
138
+ // 必须整组一起判定,否则同一单元会被拦腰劈开(半个技能目录 / 半个会话目录)。
139
+ // `considered` 保序记录,收尾按它还原改造前的文件顺序(未触发闸门时逐项一致)。
140
+ const considered: string[] = [];
141
+ const unitOrder: string[] = [];
142
+ const unitEntries = new Map<string, { rel: string; relPath: string }[]>();
88
143
  for (const rel of rels) {
89
144
  const relPath = this.relPathOf(rel);
90
145
  const unitId = this.unitIdOf(relPath);
91
146
  if (keep !== null && !keep.has(unitId)) continue;
92
147
  if (!unitAllowed(allow, `${this.id}:${unitId}`)) continue;
93
- const data = await ctx.fs.readFile(rel);
94
- files.push({ relativePath: relPath, data, contentHash: sha256Hex(data) });
148
+ let members = unitEntries.get(unitId);
149
+ if (members === undefined) {
150
+ members = [];
151
+ unitEntries.set(unitId, members);
152
+ unitOrder.push(unitId);
153
+ }
154
+ members.push({ rel, relPath });
155
+ considered.push(rel);
156
+ }
157
+
158
+ // ② 逐单元读取 + 累计字节闸门(审计 core-flow F-12 / sync#F-12)。超出 sectionByteLimit() 的
159
+ // 单元**整块**不进备份(仓库语义:单元不可拆分 —— 宁可少带,不可半带);一旦判定该单元放不下,
160
+ // 它的剩余文件不再读盘(省 I/O,也不为「统计精确」多吃内存)。放不下的单元跳过后**继续尝试
161
+ // 后面的单元**:一个巨型会话不该把它之后的所有内容一起带走(后面的小单元仍可能入选)。
162
+ const limit = this.sectionByteLimit();
163
+ let usedBytes = 0;
164
+ const droppedUnits: string[] = [];
165
+ const keptByRel = new Map<string, FilesSection['files'][number]>();
166
+ for (const unitId of unitOrder) {
167
+ const room = limit - usedBytes;
168
+ const pending: { rel: string; file: FilesSection['files'][number] }[] = [];
169
+ let unitBytes = 0;
170
+ for (const member of unitEntries.get(unitId) ?? []) {
171
+ const data = await ctx.fs.readFile(member.rel);
172
+ unitBytes += data.byteLength;
173
+ if (unitBytes > room) break;
174
+ pending.push({ rel: member.rel, file: { relativePath: member.relPath, data, contentHash: sha256Hex(data) } });
175
+ }
176
+ if (unitBytes > room) {
177
+ droppedUnits.push(unitId);
178
+ continue;
179
+ }
180
+ usedBytes += unitBytes;
181
+ for (const p of pending) keptByRel.set(p.rel, p.file);
182
+ }
183
+ // ③ 按 ① 的顺序落盘(未触发闸门时与改造前的文件顺序逐项一致)
184
+ for (const rel of considered) {
185
+ const file = keptByRel.get(rel);
186
+ if (file !== undefined) files.push(file);
187
+ }
188
+ // 绝不静默:被整块剔除的单元写进告警(导出报告可见),并给出上限与实际保留量
189
+ if (droppedUnits.length > 0) {
190
+ const shown = droppedUnits.slice(0, MAX_UNITS_PER_GATE_WARNING);
191
+ const rest = droppedUnits.length - shown.length;
192
+ warnings.push(msgOf(ctx)('adapter.fileSectionByteLimit', {
193
+ type: this.displayName,
194
+ limit: formatBytes(limit),
195
+ kept: formatBytes(usedBytes),
196
+ count: String(droppedUnits.length),
197
+ detail: (rest > 0 ? [...shown, `(+ ${rest})`] : shown).join(', '),
198
+ }));
95
199
  }
96
200
  if (rels.length === 0) warnings.push(msgOf(ctx)('adapter.dirEmpty', { type: this.displayName }));
97
201
  warnings.push(...linkWarnings(msgOf(ctx), this.displayName, listing));
@@ -167,24 +271,32 @@ export abstract class FileCollectionAdapter implements ConfigAdapter<FilesSectio
167
271
  return { ok: true };
168
272
  }
169
273
 
274
+ /**
275
+ * 结构校验(5 个文件类分区共用:skills / agentPresets / agentInstructions / sessions / self)。
276
+ *
277
+ * **整段样板都接入共享骨架 `validateJsonSection`** —— t7 接入 version 守卫与形状检查收尾,t11 接入
278
+ * object 守卫;dataVersion 由注册表按 `this.id` 派生,等价于此前的字面量 `!== 1`
279
+ * (SECTION_DATA_VERSION = 1)。本类里已无手写样板。
280
+ *
281
+ * object 守卫的**文案**为什么还要参数:文件类分区历史上用的是**专用消息键**
282
+ * `adapter.validate.fileSection`('文件分区数据必须是对象',不含 `{subject}` 占位符),与骨架缺省的
283
+ * `adapter.validate.object` + `{subject}` 是**键**级差异 —— 第 5 参 subject 补不了它。t11 给骨架加了
284
+ * 可选第 6 参 `objectMessageKey`,这里传专用键:5 个分区的用户可见报错文案与改造前**逐字相同**,
285
+ * 同时不放任何手写样板。等价由 src/adapters/file-collection.test.ts 与 outputs/t7-equiv/ 的差分脚本钉住。
286
+ */
170
287
  async validate(data: FilesSection, msg: MsgFunc = zhMsg): Promise<ValidationResult> {
171
- const issues: ValidationResult['issues'] = [];
172
- if (data === null || typeof data !== 'object') {
173
- return { valid: false, issues: [{ path: '$', message: msg('adapter.validate.fileSection'), severity: 'error' }] };
174
- }
175
- if (data.version !== 1) {
176
- issues.push({ path: 'version', message: msg('adapter.validate.version', { value: String(data.version) }), severity: 'error' });
177
- }
178
- if (!Array.isArray(data.files)) {
179
- issues.push({ path: 'files', message: msg('adapter.validate.array', { subject: 'files' }), severity: 'error' });
180
- } else {
181
- for (const f of data.files) {
182
- if (f === null || typeof f !== 'object' || typeof f.relativePath !== 'string' || f.relativePath === '') {
183
- issues.push({ path: 'files[]', message: msg('adapter.validate.fileRelativePath'), severity: 'error' });
288
+ // 第 5 参 subject 传 this.id 即骨架缺省语义(专用键不含 {subject},取值不影响输出)
289
+ return validateJsonSection<FilesSection>(this.id, data, msg, (section, issues) => {
290
+ if (!Array.isArray(section.files)) {
291
+ issues.push({ path: 'files', message: msg('adapter.validate.array', { subject: 'files' }), severity: 'error' });
292
+ } else {
293
+ for (const f of section.files) {
294
+ if (f === null || typeof f !== 'object' || typeof f.relativePath !== 'string' || f.relativePath === '') {
295
+ issues.push({ path: 'files[]', message: msg('adapter.validate.fileRelativePath'), severity: 'error' });
296
+ }
184
297
  }
185
298
  }
186
- }
187
- return { valid: issues.filter((i) => i.severity === 'error').length === 0, issues };
299
+ }, this.id, 'adapter.validate.fileSection');
188
300
  }
189
301
  }
190
302
 
@@ -7,7 +7,7 @@
7
7
  * manifest 中不出现这些 section,产品 UI 说明「DSH 当前无此配置」;
8
8
  * rules 概念已由 agentInstructions(~/.dsh/AGENTS.md)承接。
9
9
  */
10
- import type { ConfigAdapter, HostContext } from '../core/types.ts';
10
+ import type { ConfigAdapter } from '../core/types.ts';
11
11
  import { SettingsAdapter, type NamespaceProvider } from './settings.ts';
12
12
  import { UiAdapter } from './ui.ts';
13
13
  import { ProvidersAdapter } from './providers.ts';
@@ -92,3 +92,14 @@ export { PluginFilesAdapter, DEFAULT_PLUGIN_FILE_WHITELIST } from './plugin-file
92
92
  export { SessionsAdapter } from './sessions.ts';
93
93
  export { SelfAdapter, SELF_CONFIG_FILES } from './self.ts';
94
94
  export { FileCollectionAdapter } from './file-collection.ts';
95
+ /* JSON 分区共享校验骨架(本导出是其接入点)。收敛轨迹:
96
+ * · t31 抽出骨架,settings / ui / providers / plugins / mcp / prompts / credentials /
97
+ * pluginFiles 八个分区的样板并入;
98
+ * · t7 接上 workspaces.ts 与 file-collection.ts 基类的 version 守卫 + 形状检查收尾;
99
+ * · t11 给骨架加了可选第 6 参 `objectMessageKey`(object 错误的文案键,缺省
100
+ * `adapter.validate.object`),file-collection 的 object 守卫也并入并沿用文件类分区原专用键
101
+ * `adapter.validate.fileSection`,故 5 个文件类分区的用户可见报错文案逐字不变。
102
+ * 现状:**9 个分区全部走共享骨架**(settings / ui / providers / plugins / mcp / prompts /
103
+ * credentialsStatus / pluginFiles / workspaces),文件类基类(skills / agentPresets /
104
+ * agentInstructions / sessions / self)同样走它 —— src/adapters 下不再有任何手写 validate 样板。 */
105
+ export { validateJsonSection, type SectionIssues } from './json-section.ts';
@@ -0,0 +1,73 @@
1
+ /**
2
+ * JSON 分区 adapter 的共享骨架(t31,批次 4)。
3
+ *
4
+ * 为什么存在:改动前 9 个「有 JSON 载荷」的分区的 `validate()` 都以**同一段样板**开头 ——
5
+ * 先是 object 守卫,再是 `data.version !== 1` 守卫,最后才是各分区自己的载荷形状检查;
6
+ * 这段样板在每个文件里逐字抄了一遍(审计 sync#F-08 / infra#C1)。本模块把它收敛为一份,
7
+ * 并让「期望版本」直接取自注册表 `dataVersion`(不再各写 `!== 1` 字面量)。
8
+ *
9
+ * 与注册表的分工:结构校验的**描述符**(zipPath / 载荷形态 / dataVersion)在
10
+ * `src/schema/section-registry.ts`;本模块只提供「按该描述符执行校验」的通用算法,
11
+ * 各 adapter 仍负责自己特有的形状检查(用 `shape` 回调注入)。
12
+ *
13
+ * 分层:只 import schema 与 core 的类型 —— 不 import node 内置模块(client bundle 自包含铁律)。
14
+ */
15
+ import { sectionMeta } from '../schema/section-registry.ts';
16
+ import type { SectionId } from '../schema/types.ts';
17
+ import type { MsgFunc, zh } from '../core/messages.ts';
18
+ import type { ValidationResult } from '../core/types.ts';
19
+
20
+ /** 校验问题列表(各 adapter 的形状检查直接 push 到这里,与历史 `issues` 数组同一形状)。 */
21
+ export type SectionIssues = ValidationResult['issues'];
22
+
23
+ /**
24
+ * JSON 分区通用结构校验骨架。
25
+ *
26
+ * 执行顺序(与改造前逐字一致,保证行为等价):
27
+ * 1. object 守卫:非对象 → **直接返回**单条 `objectMessageKey` 错误(缺省 `adapter.validate.object`;后续检查不再执行);
28
+ * 2. version 守卫:`version !== sectionMeta(sectionId).dataVersion` → push 一条 `version` 错误,
29
+ * **不提前返回**(继续做形状检查,与历史实现一致);
30
+ * 3. `shape` 回调:分区特有的载荷形状检查(可选)。
31
+ *
32
+ * @param sectionId 分区 id —— 同时是 object 错误的默认 subject 与 dataVersion 的查表键
33
+ * @param data 反序列化后的分区载荷(来自不可信备份 → 必须按 unknown 处理)
34
+ * @param msg 文案翻译器(adapter 的 `validate(data, msg = zhMsg)` 原样传入)
35
+ * @param shape 分区特有的形状检查(在 version 守卫之后调用)
36
+ * @param subject object 错误里的显示名;缺省 = sectionId。历史上 credentials 用的是
37
+ * `'credentials'`(而非分区 id `credentialsStatus`),此参数为保留该原文案而存在 ——
38
+ * 改文案属独立的行为变更,不在本任务射程内。
39
+ * @param objectMessageKey object 错误的**消息键**(t11 新增,可选);缺省 `adapter.validate.object`
40
+ * (`'{subject} 数据必须是对象'`)。为什么需要它:文件类分区历史上用的是专用键
41
+ * `adapter.validate.fileSection`(`'文件分区数据必须是对象'`,**不含** `{subject}` 占位符),
42
+ * 那是**键**级差异 —— `subject` 参数补不了它(把 subject 拼成"文件分区"后中文模板仍多一个空格),
43
+ * 而硬换键会让 skills / agentPresets / agentInstructions / sessions / self 五个分区的用户可见
44
+ * 报错文案一起变。有了这个可选键,那处也能并入骨架而文案逐字不变。
45
+ * 键必须存在于 `src/core/messages.ts` 目录(`MsgFunc` 形参是 string,编译器只能校验到键名集合)。
46
+ */
47
+ export function validateJsonSection<S extends { version?: unknown }>(
48
+ sectionId: SectionId,
49
+ data: unknown,
50
+ msg: MsgFunc,
51
+ shape?: (section: S, issues: SectionIssues) => void,
52
+ subject: string = sectionId,
53
+ objectMessageKey: keyof typeof zh = 'adapter.validate.object',
54
+ ): ValidationResult {
55
+ if (data === null || typeof data !== 'object') {
56
+ return {
57
+ valid: false,
58
+ issues: [{ path: '$', message: msg(objectMessageKey, { subject }), severity: 'error' }],
59
+ };
60
+ }
61
+ const section = data as S;
62
+ const issues: SectionIssues = [];
63
+ const expectedVersion: unknown = sectionMeta(sectionId).dataVersion;
64
+ if (section.version !== expectedVersion) {
65
+ issues.push({
66
+ path: 'version',
67
+ message: msg('adapter.validate.version', { value: String(section.version) }),
68
+ severity: 'error',
69
+ });
70
+ }
71
+ if (shape !== undefined) shape(section, issues);
72
+ return { valid: issues.filter((i) => i.severity === 'error').length === 0, issues };
73
+ }
@@ -9,7 +9,9 @@
9
9
  import { isDeepStrictEqual } from 'node:util';
10
10
  import { msgOf, zhMsg } from '../core/messages.ts';
11
11
  import type { MsgFunc } from '../core/messages.ts';
12
- import type { McpServerEntry, McpSection } from '../schema/types.ts';
12
+ import type { McpServerEntry } from '../schema/types.ts';
13
+ import { sectionMeta } from '../schema/section-registry.ts';
14
+ import { validateJsonSection } from './json-section.ts';
13
15
  import type {
14
16
  ApplyResult, ConfigAdapter, ExportOptions, ExportSection, HostContext,
15
17
  ImportContext, PlanItem, ValidationResult,
@@ -87,9 +89,10 @@ function newLineId(serverName: string): string {
87
89
 
88
90
  export class McpAdapter implements ConfigAdapter<McpExportSection> {
89
91
  readonly id = 'mcp' as const;
90
- readonly displayName = 'MCP Servers';
91
- readonly defaultIncluded = true;
92
- readonly portability = 'platformSpecific' as const;
92
+ // 元数据唯一来源 = 注册表(t31):不再与 ui/export-flow.ts 的导出目录各写一份
93
+ readonly displayName = sectionMeta('mcp').displayName;
94
+ readonly defaultIncluded = sectionMeta('mcp').defaultIncluded;
95
+ readonly portability = sectionMeta('mcp').portability;
93
96
 
94
97
  async export(ctx: HostContext, _options: ExportOptions): Promise<ExportSection<McpExportSection>> {
95
98
  const warnings: string[] = [];
@@ -164,22 +167,16 @@ export class McpAdapter implements ConfigAdapter<McpExportSection> {
164
167
  }
165
168
 
166
169
  async validate(data: McpExportSection, msg: MsgFunc = zhMsg): Promise<ValidationResult> {
167
- const issues: ValidationResult['issues'] = [];
168
- if (data === null || typeof data !== 'object') {
169
- return { valid: false, issues: [{ path: '$', message: msg('adapter.validate.object', { subject: 'mcp' }), severity: 'error' }] };
170
- }
171
- if (data.version !== 1) {
172
- issues.push({ path: 'version', message: msg('adapter.validate.version', { value: String(data.version) }), severity: 'error' });
173
- }
174
- if (!Array.isArray(data.servers)) {
175
- issues.push({ path: 'servers', message: msg('adapter.validate.array', { subject: 'servers' }), severity: 'error' });
176
- } else {
177
- for (const s of data.servers) {
178
- if (s === null || typeof s !== 'object' || typeof s.serverName !== 'string' || s.serverName === '') {
179
- issues.push({ path: 'servers[]', message: msg('adapter.validate.serverName'), severity: 'error' });
170
+ return validateJsonSection<McpExportSection>('mcp', data, msg, (section, issues) => {
171
+ if (!Array.isArray(section.servers)) {
172
+ issues.push({ path: 'servers', message: msg('adapter.validate.array', { subject: 'servers' }), severity: 'error' });
173
+ } else {
174
+ for (const s of section.servers) {
175
+ if (s === null || typeof s !== 'object' || typeof s.serverName !== 'string' || s.serverName === '') {
176
+ issues.push({ path: 'servers[]', message: msg('adapter.validate.serverName'), severity: 'error' });
177
+ }
180
178
  }
181
179
  }
182
- }
183
- return { valid: issues.filter((i) => i.severity === 'error').length === 0, issues };
180
+ });
184
181
  }
185
182
  }
@@ -18,14 +18,17 @@ import type {
18
18
  ImportContext, PlanItem, ValidationResult,
19
19
  } from '../core/types.ts';
20
20
  import { unitAllowed, unitsFromFiles } from './units.ts';
21
+ import { sectionMeta } from '../schema/section-registry.ts';
22
+ import { validateJsonSection } from './json-section.ts';
21
23
 
22
24
  export const DEFAULT_PLUGIN_FILE_WHITELIST: readonly string[] = ['dsh-ssh.json', 'pet.json'];
23
25
 
24
26
  export class PluginFilesAdapter implements ConfigAdapter<FilesSection> {
25
27
  readonly id = 'pluginFiles' as const;
26
- readonly displayName = 'Plugin Files';
27
- readonly defaultIncluded = false;
28
- readonly portability = 'deviceSpecific' as const;
28
+ // 元数据唯一来源 = 注册表(t31):不再与 ui/export-flow.ts 的导出目录各写一份
29
+ readonly displayName = sectionMeta('pluginFiles').displayName;
30
+ readonly defaultIncluded = sectionMeta('pluginFiles').defaultIncluded;
31
+ readonly portability = sectionMeta('pluginFiles').portability;
29
32
  private readonly whitelist: string[];
30
33
  /** 约定配置目录(相对 ~/.dsh 根,如 'plugin-config');递归收集其下所有文件。undefined = 不收集。 */
31
34
  private readonly collectDir?: string;
@@ -142,16 +145,10 @@ export class PluginFilesAdapter implements ConfigAdapter<FilesSection> {
142
145
  }
143
146
 
144
147
  async validate(data: FilesSection, msg: MsgFunc = zhMsg): Promise<ValidationResult> {
145
- const issues: ValidationResult['issues'] = [];
146
- if (data === null || typeof data !== 'object') {
147
- return { valid: false, issues: [{ path: '$', message: msg('adapter.validate.object', { subject: 'pluginFiles' }), severity: 'error' }] };
148
- }
149
- if (data.version !== 1) {
150
- issues.push({ path: 'version', message: msg('adapter.validate.version', { value: String(data.version) }), severity: 'error' });
151
- }
152
- if (!Array.isArray(data.files)) {
153
- issues.push({ path: 'files', message: msg('adapter.validate.array', { subject: 'files' }), severity: 'error' });
154
- }
155
- return { valid: issues.filter((i) => i.severity === 'error').length === 0, issues };
148
+ return validateJsonSection<FilesSection>('pluginFiles', data, msg, (section, issues) => {
149
+ if (!Array.isArray(section.files)) {
150
+ issues.push({ path: 'files', message: msg('adapter.validate.array', { subject: 'files' }), severity: 'error' });
151
+ }
152
+ });
156
153
  }
157
154
  }