@logictan/dsh-config-manager 0.1.60

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 (429) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +105 -0
  3. package/cordis.patch.yml +21 -0
  4. package/lib/adapters/agent-instructions.d.ts +13 -0
  5. package/lib/adapters/agent-instructions.js +46 -0
  6. package/lib/adapters/agent-presets.d.ts +13 -0
  7. package/lib/adapters/agent-presets.js +14 -0
  8. package/lib/adapters/credentials.d.ts +37 -0
  9. package/lib/adapters/credentials.js +122 -0
  10. package/lib/adapters/file-collection.d.ts +15 -0
  11. package/lib/adapters/file-collection.js +127 -0
  12. package/lib/adapters/index.d.ts +57 -0
  13. package/lib/adapters/index.js +55 -0
  14. package/lib/adapters/link-report.d.ts +16 -0
  15. package/lib/adapters/link-report.js +47 -0
  16. package/lib/adapters/mcp.d.ts +28 -0
  17. package/lib/adapters/mcp.js +178 -0
  18. package/lib/adapters/plugin-files.d.ts +18 -0
  19. package/lib/adapters/plugin-files.js +142 -0
  20. package/lib/adapters/plugins.d.ts +41 -0
  21. package/lib/adapters/plugins.js +603 -0
  22. package/lib/adapters/pnpm-workspace.d.ts +26 -0
  23. package/lib/adapters/pnpm-workspace.js +147 -0
  24. package/lib/adapters/prompts.d.ts +30 -0
  25. package/lib/adapters/prompts.js +231 -0
  26. package/lib/adapters/providers.d.ts +34 -0
  27. package/lib/adapters/providers.js +155 -0
  28. package/lib/adapters/self.d.ts +17 -0
  29. package/lib/adapters/self.js +71 -0
  30. package/lib/adapters/sessions.d.ts +14 -0
  31. package/lib/adapters/sessions.js +15 -0
  32. package/lib/adapters/settings.d.ts +28 -0
  33. package/lib/adapters/settings.js +165 -0
  34. package/lib/adapters/skills.d.ts +13 -0
  35. package/lib/adapters/skills.js +14 -0
  36. package/lib/adapters/test-helpers.d.ts +125 -0
  37. package/lib/adapters/test-helpers.js +236 -0
  38. package/lib/adapters/ui.d.ts +27 -0
  39. package/lib/adapters/ui.js +89 -0
  40. package/lib/adapters/workspaces.d.ts +13 -0
  41. package/lib/adapters/workspaces.js +106 -0
  42. package/lib/client.d.ts +1026 -0
  43. package/lib/client.js +10047 -0
  44. package/lib/core/analyzer.d.ts +99 -0
  45. package/lib/core/analyzer.js +934 -0
  46. package/lib/core/backup.d.ts +138 -0
  47. package/lib/core/backup.js +513 -0
  48. package/lib/core/boot-rescue.d.ts +146 -0
  49. package/lib/core/boot-rescue.js +618 -0
  50. package/lib/core/cache-cleaner.d.ts +31 -0
  51. package/lib/core/cache-cleaner.js +95 -0
  52. package/lib/core/config-lifecycle.d.ts +180 -0
  53. package/lib/core/config-lifecycle.js +481 -0
  54. package/lib/core/config-snapshot.d.ts +135 -0
  55. package/lib/core/config-snapshot.js +424 -0
  56. package/lib/core/config-state.d.ts +70 -0
  57. package/lib/core/config-state.js +177 -0
  58. package/lib/core/consult-source.d.ts +58 -0
  59. package/lib/core/consult-source.js +260 -0
  60. package/lib/core/crash-report.d.ts +84 -0
  61. package/lib/core/crash-report.js +317 -0
  62. package/lib/core/exporter.d.ts +68 -0
  63. package/lib/core/exporter.js +375 -0
  64. package/lib/core/ghost-sweep.d.ts +19 -0
  65. package/lib/core/ghost-sweep.js +52 -0
  66. package/lib/core/importer.d.ts +60 -0
  67. package/lib/core/importer.js +29 -0
  68. package/lib/core/index.d.ts +14 -0
  69. package/lib/core/index.js +14 -0
  70. package/lib/core/journal.d.ts +197 -0
  71. package/lib/core/journal.js +418 -0
  72. package/lib/core/local-plugin-host.d.ts +35 -0
  73. package/lib/core/local-plugin-host.js +120 -0
  74. package/lib/core/local-plugin-pack.d.ts +135 -0
  75. package/lib/core/local-plugin-pack.js +278 -0
  76. package/lib/core/messages.d.ts +310 -0
  77. package/lib/core/messages.js +621 -0
  78. package/lib/core/migration-consult.d.ts +117 -0
  79. package/lib/core/migration-consult.js +363 -0
  80. package/lib/core/migration-history.d.ts +153 -0
  81. package/lib/core/migration-history.js +411 -0
  82. package/lib/core/model-tools.d.ts +50 -0
  83. package/lib/core/model-tools.js +148 -0
  84. package/lib/core/msg-types.d.ts +8 -0
  85. package/lib/core/msg-types.js +6 -0
  86. package/lib/core/phase3-child-crash.d.ts +1 -0
  87. package/lib/core/phase3-child-crash.js +87 -0
  88. package/lib/core/phase3-host.d.ts +138 -0
  89. package/lib/core/phase3-host.js +359 -0
  90. package/lib/core/phase3-prod-child.d.ts +1 -0
  91. package/lib/core/phase3-prod-child.js +35 -0
  92. package/lib/core/phase4-crash-child.d.ts +1 -0
  93. package/lib/core/phase4-crash-child.js +103 -0
  94. package/lib/core/plugin-cli.d.ts +150 -0
  95. package/lib/core/plugin-cli.js +497 -0
  96. package/lib/core/reconcile.d.ts +120 -0
  97. package/lib/core/reconcile.js +389 -0
  98. package/lib/core/recovery-orchestrator.d.ts +94 -0
  99. package/lib/core/recovery-orchestrator.js +327 -0
  100. package/lib/core/restore.d.ts +162 -0
  101. package/lib/core/restore.js +703 -0
  102. package/lib/core/rollback.d.ts +12 -0
  103. package/lib/core/rollback.js +145 -0
  104. package/lib/core/run-registry.d.ts +85 -0
  105. package/lib/core/run-registry.js +157 -0
  106. package/lib/core/startup-barrier.d.ts +53 -0
  107. package/lib/core/startup-barrier.js +77 -0
  108. package/lib/core/types.d.ts +515 -0
  109. package/lib/core/types.js +27 -0
  110. package/lib/core/undo.d.ts +73 -0
  111. package/lib/core/undo.js +87 -0
  112. package/lib/core/validator.d.ts +18 -0
  113. package/lib/core/validator.js +77 -0
  114. package/lib/core/verify-recovery.d.ts +21 -0
  115. package/lib/core/verify-recovery.js +256 -0
  116. package/lib/core/watcher.d.ts +101 -0
  117. package/lib/core/watcher.js +219 -0
  118. package/lib/index.d.ts +238 -0
  119. package/lib/index.js +2764 -0
  120. package/lib/migrations/index.d.ts +29 -0
  121. package/lib/migrations/index.js +56 -0
  122. package/lib/migrations/v1-to-v2.d.ts +9 -0
  123. package/lib/migrations/v1-to-v2.js +10 -0
  124. package/lib/profiles/index.d.ts +4 -0
  125. package/lib/profiles/index.js +5 -0
  126. package/lib/profiles/profile-manager.d.ts +106 -0
  127. package/lib/profiles/profile-manager.js +543 -0
  128. package/lib/schema/config.d.ts +28 -0
  129. package/lib/schema/config.js +165 -0
  130. package/lib/schema/index.d.ts +24 -0
  131. package/lib/schema/index.js +26 -0
  132. package/lib/schema/manifest.d.ts +28 -0
  133. package/lib/schema/manifest.js +109 -0
  134. package/lib/schema/tombstones.d.ts +46 -0
  135. package/lib/schema/tombstones.js +64 -0
  136. package/lib/schema/types.d.ts +232 -0
  137. package/lib/schema/types.js +8 -0
  138. package/lib/schema/versions.d.ts +27 -0
  139. package/lib/schema/versions.js +50 -0
  140. package/lib/security/encryption.d.ts +85 -0
  141. package/lib/security/encryption.js +279 -0
  142. package/lib/security/index.d.ts +16 -0
  143. package/lib/security/index.js +17 -0
  144. package/lib/security/integrity.d.ts +30 -0
  145. package/lib/security/integrity.js +78 -0
  146. package/lib/security/redaction.d.ts +26 -0
  147. package/lib/security/redaction.js +95 -0
  148. package/lib/security/secret-scanner.d.ts +86 -0
  149. package/lib/security/secret-scanner.js +404 -0
  150. package/lib/security/vault.d.ts +59 -0
  151. package/lib/security/vault.js +151 -0
  152. package/lib/security/zip-security.d.ts +27 -0
  153. package/lib/security/zip-security.js +154 -0
  154. package/lib/sync/autosync-config.d.ts +39 -0
  155. package/lib/sync/autosync-config.js +151 -0
  156. package/lib/sync/autosync-scheduler.d.ts +139 -0
  157. package/lib/sync/autosync-scheduler.js +564 -0
  158. package/lib/sync/fs.d.ts +18 -0
  159. package/lib/sync/fs.js +49 -0
  160. package/lib/sync/git/git-transport.d.ts +100 -0
  161. package/lib/sync/git/git-transport.js +437 -0
  162. package/lib/sync/github-auth.d.ts +131 -0
  163. package/lib/sync/github-auth.js +264 -0
  164. package/lib/sync/layout.d.ts +43 -0
  165. package/lib/sync/layout.js +203 -0
  166. package/lib/sync/snapshot-json.d.ts +9 -0
  167. package/lib/sync/snapshot-json.js +107 -0
  168. package/lib/sync/sync-config.d.ts +81 -0
  169. package/lib/sync/sync-config.js +281 -0
  170. package/lib/sync/sync-engine.d.ts +269 -0
  171. package/lib/sync/sync-engine.js +591 -0
  172. package/lib/sync/sync-history.d.ts +42 -0
  173. package/lib/sync/sync-history.js +78 -0
  174. package/lib/sync/sync-selection.d.ts +29 -0
  175. package/lib/sync/sync-selection.js +127 -0
  176. package/lib/sync/sync-session.d.ts +40 -0
  177. package/lib/sync/sync-session.js +54 -0
  178. package/lib/sync/sync-state.d.ts +37 -0
  179. package/lib/sync/sync-state.js +108 -0
  180. package/lib/sync/transport.d.ts +75 -0
  181. package/lib/sync/transport.js +55 -0
  182. package/lib/sync/ui-prefs.d.ts +34 -0
  183. package/lib/sync/ui-prefs.js +109 -0
  184. package/lib/sync/webdav/webdav-transport.d.ts +77 -0
  185. package/lib/sync/webdav/webdav-transport.js +451 -0
  186. package/lib/ui/errors.d.ts +19 -0
  187. package/lib/ui/errors.js +81 -0
  188. package/lib/ui/export-flow.d.ts +82 -0
  189. package/lib/ui/export-flow.js +98 -0
  190. package/lib/ui/i18n.d.ts +289 -0
  191. package/lib/ui/i18n.js +581 -0
  192. package/lib/ui/migration-consult-view.d.ts +91 -0
  193. package/lib/ui/migration-consult-view.js +115 -0
  194. package/lib/ui/progress.d.ts +38 -0
  195. package/lib/ui/progress.js +87 -0
  196. package/lib/ui/report.d.ts +23 -0
  197. package/lib/ui/report.js +153 -0
  198. package/lib/ui/test-helpers.d.ts +69 -0
  199. package/lib/ui/test-helpers.js +154 -0
  200. package/lib/ui/types.d.ts +257 -0
  201. package/lib/ui/types.js +21 -0
  202. package/lib/utils/atomic-write.d.ts +88 -0
  203. package/lib/utils/atomic-write.js +399 -0
  204. package/lib/utils/env-lock.d.ts +393 -0
  205. package/lib/utils/env-lock.js +930 -0
  206. package/lib/utils/hashing.d.ts +17 -0
  207. package/lib/utils/hashing.js +41 -0
  208. package/lib/utils/json.d.ts +24 -0
  209. package/lib/utils/json.js +76 -0
  210. package/lib/utils/logger.d.ts +39 -0
  211. package/lib/utils/logger.js +82 -0
  212. package/lib/utils/paths.d.ts +42 -0
  213. package/lib/utils/paths.js +189 -0
  214. package/lib/utils/proxy.d.ts +69 -0
  215. package/lib/utils/proxy.js +455 -0
  216. package/lib/utils/recursive-walk.d.ts +24 -0
  217. package/lib/utils/recursive-walk.js +134 -0
  218. package/lib/utils/zip.d.ts +54 -0
  219. package/lib/utils/zip.js +285 -0
  220. package/package.json +188 -0
  221. package/src/adapters/agent-instructions.ts +48 -0
  222. package/src/adapters/agent-presets.ts +14 -0
  223. package/src/adapters/credentials.test.ts +90 -0
  224. package/src/adapters/credentials.ts +143 -0
  225. package/src/adapters/file-collection.ts +137 -0
  226. package/src/adapters/files.test.ts +353 -0
  227. package/src/adapters/index.ts +94 -0
  228. package/src/adapters/link-report.test.ts +65 -0
  229. package/src/adapters/link-report.ts +61 -0
  230. package/src/adapters/mcp.test.ts +96 -0
  231. package/src/adapters/mcp.ts +185 -0
  232. package/src/adapters/plugin-files.ts +146 -0
  233. package/src/adapters/plugins.test.ts +326 -0
  234. package/src/adapters/plugins.ts +615 -0
  235. package/src/adapters/pnpm-workspace.test.ts +87 -0
  236. package/src/adapters/pnpm-workspace.ts +150 -0
  237. package/src/adapters/prompts.test.ts +110 -0
  238. package/src/adapters/prompts.ts +230 -0
  239. package/src/adapters/providers.test.ts +126 -0
  240. package/src/adapters/providers.ts +173 -0
  241. package/src/adapters/roundtrip.test.ts +190 -0
  242. package/src/adapters/self.test.ts +92 -0
  243. package/src/adapters/self.ts +75 -0
  244. package/src/adapters/sessions.ts +15 -0
  245. package/src/adapters/settings.test.ts +100 -0
  246. package/src/adapters/settings.ts +187 -0
  247. package/src/adapters/skills.ts +14 -0
  248. package/src/adapters/test-helpers.ts +248 -0
  249. package/src/adapters/ui.test.ts +57 -0
  250. package/src/adapters/ui.ts +108 -0
  251. package/src/adapters/workspaces.test.ts +106 -0
  252. package/src/adapters/workspaces.ts +109 -0
  253. package/src/client/ConfigManagerSection.tsx +90 -0
  254. package/src/client/api.ts +66 -0
  255. package/src/client/client-types.ts +18 -0
  256. package/src/client/common/ErrorBanner.tsx +73 -0
  257. package/src/client/common/Icon.tsx +129 -0
  258. package/src/client/common/Modal.tsx +173 -0
  259. package/src/client/common/ToastViewport.tsx +73 -0
  260. package/src/client/common/toast-store.test.ts +192 -0
  261. package/src/client/common/toast-store.ts +181 -0
  262. package/src/client/common/toast-types.ts +7 -0
  263. package/src/client/common/ui.tsx +342 -0
  264. package/src/client/config-manager.module.css +2971 -0
  265. package/src/client/css-modules.d.ts +8 -0
  266. package/src/client/index.ts +65 -0
  267. package/src/client/locales.ts +56 -0
  268. package/src/client/lucide-icons.d.ts +38 -0
  269. package/src/client/run-store.test.ts +223 -0
  270. package/src/client/run-store.ts +372 -0
  271. package/src/client/sync/SyncConfirmView.tsx +379 -0
  272. package/src/client/sync/SyncHistoryView.test.ts +40 -0
  273. package/src/client/sync/SyncHistoryView.tsx +220 -0
  274. package/src/client/sync/SyncSettingsView.tsx +1535 -0
  275. package/src/client/sync/history-model.test.ts +194 -0
  276. package/src/client/sync/history-model.ts +195 -0
  277. package/src/client/sync/sync-api.test.ts +585 -0
  278. package/src/client/sync/sync-api.ts +543 -0
  279. package/src/client/sync/sync-locales.ts +416 -0
  280. package/src/client/sync/sync-push-preview.test.ts +52 -0
  281. package/src/client/sync/sync-view-v2.test.ts +213 -0
  282. package/src/client/sync/sync-view.test.ts +487 -0
  283. package/src/client/sync/sync-view.ts +759 -0
  284. package/src/core/analyzer.ts +1068 -0
  285. package/src/core/backup.ts +605 -0
  286. package/src/core/boot-rescue.test.ts +847 -0
  287. package/src/core/boot-rescue.ts +737 -0
  288. package/src/core/cache-cleaner.test.ts +169 -0
  289. package/src/core/cache-cleaner.ts +121 -0
  290. package/src/core/config-lifecycle.test.ts +727 -0
  291. package/src/core/config-lifecycle.ts +573 -0
  292. package/src/core/config-snapshot.test.ts +369 -0
  293. package/src/core/config-snapshot.ts +522 -0
  294. package/src/core/config-state.test.ts +381 -0
  295. package/src/core/config-state.ts +207 -0
  296. package/src/core/consult-source.test.ts +199 -0
  297. package/src/core/consult-source.ts +293 -0
  298. package/src/core/crash-report.test.ts +380 -0
  299. package/src/core/crash-report.ts +369 -0
  300. package/src/core/exporter.ts +417 -0
  301. package/src/core/ghost-sweep.test.ts +94 -0
  302. package/src/core/ghost-sweep.ts +67 -0
  303. package/src/core/importer.ts +83 -0
  304. package/src/core/index.ts +56 -0
  305. package/src/core/journal.test.ts +257 -0
  306. package/src/core/journal.ts +518 -0
  307. package/src/core/local-plugin-host.ts +152 -0
  308. package/src/core/local-plugin-pack.test.ts +359 -0
  309. package/src/core/local-plugin-pack.ts +363 -0
  310. package/src/core/messages.test.ts +54 -0
  311. package/src/core/messages.ts +664 -0
  312. package/src/core/migration-consult.test.ts +315 -0
  313. package/src/core/migration-consult.ts +494 -0
  314. package/src/core/migration-history.test.ts +319 -0
  315. package/src/core/migration-history.ts +484 -0
  316. package/src/core/model-tools.test.ts +190 -0
  317. package/src/core/model-tools.ts +208 -0
  318. package/src/core/msg-types.ts +10 -0
  319. package/src/core/phase3-child-crash.ts +95 -0
  320. package/src/core/phase3-host.ts +411 -0
  321. package/src/core/phase3-p1.test.ts +212 -0
  322. package/src/core/phase3-p2b-fingerprint.test.ts +189 -0
  323. package/src/core/phase3-prod-child.ts +36 -0
  324. package/src/core/phase3-production-integration.test.ts +286 -0
  325. package/src/core/phase4-crash-child.ts +101 -0
  326. package/src/core/phase4-crash-injection.test.ts +166 -0
  327. package/src/core/plugin-cli.fs.test.ts +184 -0
  328. package/src/core/plugin-cli.test.ts +137 -0
  329. package/src/core/plugin-cli.ts +563 -0
  330. package/src/core/reconcile.test.ts +392 -0
  331. package/src/core/reconcile.ts +498 -0
  332. package/src/core/recovery-orchestrator.test.ts +145 -0
  333. package/src/core/recovery-orchestrator.ts +366 -0
  334. package/src/core/restore-manage.test.ts +94 -0
  335. package/src/core/restore.ts +849 -0
  336. package/src/core/rollback.ts +151 -0
  337. package/src/core/run-progress.test.ts +237 -0
  338. package/src/core/run-registry.test.ts +232 -0
  339. package/src/core/run-registry.ts +209 -0
  340. package/src/core/smoke.test.ts +842 -0
  341. package/src/core/startup-barrier.ts +99 -0
  342. package/src/core/types.ts +542 -0
  343. package/src/core/undo.ts +116 -0
  344. package/src/core/validator.ts +84 -0
  345. package/src/core/verify-recovery.ts +265 -0
  346. package/src/core/watcher.test.ts +326 -0
  347. package/src/core/watcher.ts +263 -0
  348. package/src/index.facade.test.ts +265 -0
  349. package/src/index.sync.test.ts +217 -0
  350. package/src/index.ts +2975 -0
  351. package/src/migrations/index.ts +71 -0
  352. package/src/migrations/v1-to-v2.ts +17 -0
  353. package/src/profiles/index.ts +8 -0
  354. package/src/profiles/profile-manager.test.ts +282 -0
  355. package/src/profiles/profile-manager.ts +630 -0
  356. package/src/schema/config.ts +165 -0
  357. package/src/schema/index.ts +71 -0
  358. package/src/schema/manifest-schema.test.ts +519 -0
  359. package/src/schema/manifest.ts +124 -0
  360. package/src/schema/tombstones.test.ts +131 -0
  361. package/src/schema/tombstones.ts +89 -0
  362. package/src/schema/types.ts +245 -0
  363. package/src/schema/versions.ts +55 -0
  364. package/src/security/encryption.ts +335 -0
  365. package/src/security/index.ts +16 -0
  366. package/src/security/integrity.ts +102 -0
  367. package/src/security/redaction.ts +101 -0
  368. package/src/security/secret-scanner.ts +445 -0
  369. package/src/security/security.test.ts +1215 -0
  370. package/src/security/vault.test.ts +164 -0
  371. package/src/security/vault.ts +198 -0
  372. package/src/security/zip-security.ts +180 -0
  373. package/src/sync/autosync-config.test.ts +147 -0
  374. package/src/sync/autosync-config.ts +179 -0
  375. package/src/sync/autosync-scheduler.test.ts +490 -0
  376. package/src/sync/autosync-scheduler.ts +646 -0
  377. package/src/sync/fs.ts +48 -0
  378. package/src/sync/git/git-transport.test.ts +673 -0
  379. package/src/sync/git/git-transport.ts +491 -0
  380. package/src/sync/github-auth.test.ts +280 -0
  381. package/src/sync/github-auth.ts +356 -0
  382. package/src/sync/layout.test.ts +270 -0
  383. package/src/sync/layout.ts +222 -0
  384. package/src/sync/snapshot-json.test.ts +119 -0
  385. package/src/sync/snapshot-json.ts +128 -0
  386. package/src/sync/sync-config.test.ts +310 -0
  387. package/src/sync/sync-config.ts +299 -0
  388. package/src/sync/sync-engine.test.ts +1074 -0
  389. package/src/sync/sync-engine.ts +754 -0
  390. package/src/sync/sync-history.test.ts +116 -0
  391. package/src/sync/sync-history.ts +120 -0
  392. package/src/sync/sync-selection.test.ts +186 -0
  393. package/src/sync/sync-selection.ts +143 -0
  394. package/src/sync/sync-session.test.ts +143 -0
  395. package/src/sync/sync-session.ts +77 -0
  396. package/src/sync/sync-state.test.ts +164 -0
  397. package/src/sync/sync-state.ts +131 -0
  398. package/src/sync/transport.test.ts +111 -0
  399. package/src/sync/transport.ts +118 -0
  400. package/src/sync/ui-prefs.test.ts +152 -0
  401. package/src/sync/ui-prefs.ts +135 -0
  402. package/src/sync/webdav/webdav-transport.test.ts +871 -0
  403. package/src/sync/webdav/webdav-transport.ts +535 -0
  404. package/src/ui/errors.test.ts +47 -0
  405. package/src/ui/errors.ts +100 -0
  406. package/src/ui/export-flow.test.ts +87 -0
  407. package/src/ui/export-flow.ts +156 -0
  408. package/src/ui/i18n.ts +590 -0
  409. package/src/ui/migration-consult-view.test.ts +151 -0
  410. package/src/ui/migration-consult-view.ts +178 -0
  411. package/src/ui/progress.test.ts +47 -0
  412. package/src/ui/progress.ts +102 -0
  413. package/src/ui/report.test.ts +101 -0
  414. package/src/ui/report.ts +146 -0
  415. package/src/ui/test-helpers.ts +184 -0
  416. package/src/ui/types.ts +325 -0
  417. package/src/utils/atomic-write.test.ts +312 -0
  418. package/src/utils/atomic-write.ts +422 -0
  419. package/src/utils/env-lock.test.ts +999 -0
  420. package/src/utils/env-lock.ts +1086 -0
  421. package/src/utils/hashing.ts +53 -0
  422. package/src/utils/json.ts +77 -0
  423. package/src/utils/logger.ts +114 -0
  424. package/src/utils/paths.ts +176 -0
  425. package/src/utils/proxy.test.ts +465 -0
  426. package/src/utils/proxy.ts +493 -0
  427. package/src/utils/recursive-walk.test.ts +148 -0
  428. package/src/utils/recursive-walk.ts +145 -0
  429. package/src/utils/zip.ts +332 -0
@@ -0,0 +1,143 @@
1
+ /**
2
+ * SyncSessionStore 测试:内存登记临时 ZIP + ImportPlan,TTL 30 分钟,惰性清理,同 id 覆盖。
3
+ */
4
+ import test from 'node:test';
5
+ import assert from 'node:assert/strict';
6
+ import crypto from 'node:crypto';
7
+
8
+ import { SyncSessionStore } from './sync-session.ts';
9
+ import type { SyncConfig } from './sync-config.ts';
10
+ import type { ImportAnalysis, ImportPlan } from '../core/types.ts';
11
+ import type { SectionId } from '../schema/types.ts';
12
+
13
+ const GIT_CONFIG: SyncConfig = { schemaVersion: 2, transport: 'git', git: { repoUrl: 'https://github.com/foo/bar.git' } };
14
+
15
+ const DEFAULT_TTL_MS = 30 * 60 * 1000;
16
+
17
+ function makePlan(seed: string): ImportPlan {
18
+ return {
19
+ items: [{ id: `item-${seed}`, kind: 'Update', adapter: 'settings', description: `desc-${seed}`, severity: 'info', target: { adapter: 'settings', ref: 'general' } }],
20
+ globalStrategy: 'merge',
21
+ pathMappings: [],
22
+ missingSecrets: [],
23
+ needsRestart: false,
24
+ estimatedActions: { settings: 1 } as unknown as Record<SectionId, number>,
25
+ };
26
+ }
27
+
28
+ function makeAnalysis(seed: string): ImportAnalysis {
29
+ return {
30
+ valid: true,
31
+ errors: [],
32
+ warnings: [],
33
+ compatibility: 'excellent',
34
+ sectionsInZip: ['settings'] as SectionId[],
35
+ unsupportedSections: [],
36
+ unsupportedVersions: [],
37
+ pluginSummary: { installed: 0, toInstall: 0 },
38
+ pathIssues: [],
39
+ secretCount: 0,
40
+ dependencyIssues: [],
41
+ encrypted: false,
42
+ };
43
+ }
44
+
45
+ function makeSession(store: SyncSessionStore, opts: { seed: string; nowMs?: number }): string {
46
+ const now = opts.nowMs ?? Date.now();
47
+ const id = crypto.randomUUID();
48
+ store.set({
49
+ id,
50
+ zipPath: `/tmp/session-${opts.seed}.zip`,
51
+ plan: makePlan(opts.seed),
52
+ analysis: makeAnalysis(opts.seed),
53
+ snapshotId: `snap-${opts.seed}`,
54
+ config: GIT_CONFIG,
55
+ createdAt: now,
56
+ expiresAt: now + DEFAULT_TTL_MS,
57
+ });
58
+ return id;
59
+ }
60
+
61
+ test('SyncSessionStore: set/get 往返;同 id 覆盖', () => {
62
+ const store = new SyncSessionStore({ now: () => 5_000_000 });
63
+ const id = makeSession(store, { seed: 'a', nowMs: 5_000_000 });
64
+ const s1 = store.get(id);
65
+ assert.ok(s1, 'get 应返回 session');
66
+ assert.equal(s1!.zipPath, '/tmp/session-a.zip');
67
+ assert.equal(s1!.plan.items[0]!.id, 'item-a');
68
+ assert.equal(s1!.snapshotId, 'snap-a');
69
+
70
+ // 同 id 覆盖(仍在有效期内)
71
+ store.set({
72
+ id,
73
+ zipPath: '/tmp/session-b.zip',
74
+ plan: makePlan('b'),
75
+ analysis: makeAnalysis('b'),
76
+ snapshotId: 'snap-b',
77
+ config: GIT_CONFIG,
78
+ createdAt: 5_000_000,
79
+ expiresAt: 5_000_000 + DEFAULT_TTL_MS,
80
+ });
81
+ const s2 = store.get(id);
82
+ assert.equal(s2!.zipPath, '/tmp/session-b.zip');
83
+ assert.equal(s2!.plan.items[0]!.id, 'item-b');
84
+ assert.equal(s2!.snapshotId, 'snap-b');
85
+ });
86
+
87
+ test('SyncSessionStore: get 过期条目 → undefined(惰性清理)', () => {
88
+ const store = new SyncSessionStore();
89
+ const now = 1_000_000;
90
+ const id = crypto.randomUUID();
91
+ store.set({
92
+ id,
93
+ zipPath: '/tmp/expired.zip',
94
+ plan: makePlan('x'),
95
+ analysis: makeAnalysis('x'),
96
+ snapshotId: 'snap-x',
97
+ config: GIT_CONFIG,
98
+ createdAt: now - DEFAULT_TTL_MS - 1,
99
+ expiresAt: now - 1,
100
+ });
101
+ const s = store.get(id);
102
+ assert.equal(s, undefined, '过期条目应返回 undefined');
103
+ });
104
+
105
+ test('SyncSessionStore: delete 移除条目', () => {
106
+ const store = new SyncSessionStore({ now: () => 5_000_000 });
107
+ const id = makeSession(store, { seed: 'del', nowMs: 5_000_000 });
108
+ assert.ok(store.get(id), 'set 后存在');
109
+ store.delete(id);
110
+ assert.equal(store.get(id), undefined, 'delete 后不存在');
111
+ });
112
+
113
+ test('SyncSessionStore: TTL 边界——过期临界点', () => {
114
+ const store = new SyncSessionStore({ now: () => 2_000_000 });
115
+ const now = 2_000_000;
116
+ const id = crypto.randomUUID();
117
+ // 恰好在 expiresAt(now === expiresAt)→ 视为过期(惰性清理)
118
+ store.set({
119
+ id,
120
+ zipPath: '/tmp/boundary.zip',
121
+ plan: makePlan('boundary'),
122
+ analysis: makeAnalysis('boundary'),
123
+ snapshotId: 'snap-b',
124
+ config: GIT_CONFIG,
125
+ createdAt: now - DEFAULT_TTL_MS,
126
+ expiresAt: now,
127
+ });
128
+ assert.equal(store.get(id), undefined, 'expiresAt === now 视为过期');
129
+
130
+ // 还差 1ms → 仍有效
131
+ const id2 = crypto.randomUUID();
132
+ store.set({
133
+ id: id2,
134
+ zipPath: '/tmp/boundary2.zip',
135
+ plan: makePlan('boundary2'),
136
+ analysis: makeAnalysis('boundary2'),
137
+ snapshotId: 'snap-b2',
138
+ config: GIT_CONFIG,
139
+ createdAt: now - DEFAULT_TTL_MS,
140
+ expiresAt: now + 1,
141
+ });
142
+ assert.ok(store.get(id2), 'expiresAt > now 仍应有效');
143
+ });
@@ -0,0 +1,77 @@
1
+ /**
2
+ * SyncSessionStore:一键同步差异确认会话的内存登记。
3
+ *
4
+ * 会话把「拉取预览」与「逐项执行导入」解耦:一次 /sync/sync 在内存持有一份
5
+ * 临时 ZIP 路径 + ImportPlan + ImportAnalysis,配一个 syncSessionId;
6
+ * /sync/apply-items 凭 id 复用这些数据,避免重复拉取/重复 build ZIP。
7
+ *
8
+ * 生命周期:
9
+ * - 进程内存(不落盘),默认 TTL 30 分钟,过期条目惰性清理(get 时检查);
10
+ * - 同 id 覆盖(set 后旧 session 被新 session 替换,旧 ZIP 由上层负责清理);
11
+ * - delete 显式移除(/sync/cancel 与 apply-items 消费后调用)。
12
+ */
13
+ import crypto from 'node:crypto';
14
+ import type { ImportAnalysis, ImportPlan } from '../core/types.ts';
15
+ import type { SyncConfig } from './sync-config.ts';
16
+
17
+ /** 默认会话 TTL(30 分钟) */
18
+ export const DEFAULT_SESSION_TTL_MS = 30 * 60 * 1000;
19
+
20
+ /** 一次差异确认会话的完整数据 */
21
+ export interface SyncSession {
22
+ id: string;
23
+ /** 临时标准 ZIP(apply-items 执行 executeImportPlan 需要) */
24
+ zipPath: string;
25
+ plan: ImportPlan;
26
+ analysis: ImportAnalysis;
27
+ /** 被拉取的远端快照 id */
28
+ snapshotId: string;
29
+ /** 该次同步使用的通道配置(git/webdav 通用;apply-items 据此重建引擎) */
30
+ config: SyncConfig;
31
+ /** 创建时间(epoch ms) */
32
+ createdAt: number;
33
+ /** 过期时间(epoch ms;过期条目惰性清理) */
34
+ expiresAt: number;
35
+ }
36
+
37
+ /** 内存会话存储;同 id 覆盖;过期条目视为不存在。 */
38
+ export class SyncSessionStore {
39
+ private readonly sessions = new Map<string, SyncSession>();
40
+ private readonly ttlMs: number;
41
+ private readonly now: () => number;
42
+
43
+ constructor(opts: { ttlMs?: number; now?: () => number } = {}) {
44
+ this.ttlMs = opts.ttlMs ?? DEFAULT_SESSION_TTL_MS;
45
+ this.now = opts.now ?? (() => Date.now());
46
+ }
47
+
48
+ /** 登记(或覆盖)会话;id 缺省时自动生成。createdAt/expiresAt 缺省自动填充。返回会话 id。 */
49
+ set(session: Omit<SyncSession, 'id' | 'createdAt' | 'expiresAt'> & { id?: string; createdAt?: number; expiresAt?: number }): string {
50
+ const id = session.id ?? `sync-session-${crypto.randomUUID()}`;
51
+ const createdAt = session.createdAt ?? this.now();
52
+ const expiresAt = session.expiresAt ?? createdAt + this.ttlMs;
53
+ this.sessions.set(id, {
54
+ ...session,
55
+ id,
56
+ createdAt,
57
+ expiresAt,
58
+ });
59
+ return id;
60
+ }
61
+
62
+ /** 读取会话;过期条目返回 undefined(惰性清理)。 */
63
+ get(id: string): SyncSession | undefined {
64
+ const s = this.sessions.get(id);
65
+ if (s === undefined) return undefined;
66
+ if (this.now() >= s.expiresAt) {
67
+ this.sessions.delete(id);
68
+ return undefined;
69
+ }
70
+ return { ...s };
71
+ }
72
+
73
+ /** 删除会话(cancel / apply-items 消费后调用)。 */
74
+ delete(id: string): void {
75
+ this.sessions.delete(id);
76
+ }
77
+ }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * m-sync-transport:sync-state.json 模型测试。
3
+ * - hashSection:JSON 分区与文件类分区的内容 hash(确定性、键序无关、文件序无关)
4
+ * - saveSyncState / loadSyncState:往返、缺省、schemaVersion 校验、损坏 JSON 拒绝
5
+ */
6
+ import test from 'node:test';
7
+ import assert from 'node:assert/strict';
8
+ import fs from 'node:fs/promises';
9
+ import os from 'node:os';
10
+ import path from 'node:path';
11
+
12
+ import { hashSection, loadSyncState, saveSyncState, SYNC_STATE_FILE, SYNC_STATE_SCHEMA_VERSION } from './sync-state.ts';
13
+ import type { SyncState } from './sync-state.ts';
14
+ import type { FileEntry, SectionData } from '../schema/types.ts';
15
+
16
+ test('hashSection: JSON 分区确定性 + 键序无关(同一内容不同插入序 → 相同 hash)', () => {
17
+ const a: SectionData = { version: 1, namespaces: { general: { value: { theme: 'dark' }, revision: 1, secrets: [] } } };
18
+ const b: SectionData = {
19
+ namespaces: { general: { secrets: [], revision: 1, value: { theme: 'dark' } } },
20
+ version: 1,
21
+ } as unknown as SectionData;
22
+ assert.equal(hashSection(a), hashSection(b), '键序无关');
23
+ const c: SectionData = { version: 1, namespaces: { general: { value: { theme: 'light' }, revision: 1, secrets: [] } } };
24
+ assert.notEqual(hashSection(a), hashSection(c), '内容变化 hash 必须变化');
25
+ // 确定性:同对象多次调用
26
+ assert.equal(hashSection(a), hashSection(a));
27
+ });
28
+
29
+ test('hashSection: 文件类分区确定性 + 文件序无关,内容变化 → hash 变化', () => {
30
+ const mk = (files: { relativePath: string; data: Uint8Array; contentHash?: string }[]): SectionData => ({
31
+ version: 1,
32
+ files: files.map((f): FileEntry => ({ relativePath: f.relativePath, data: f.data, contentHash: f.contentHash ?? '' })),
33
+ });
34
+ const f1 = { relativePath: 'a.md', data: new TextEncoder().encode('# A\n') };
35
+ const f2 = { relativePath: 'b/skill.md', data: new TextEncoder().encode('# B\n') };
36
+ const x = hashSection(mk([f1, f2]));
37
+ const y = hashSection(mk([f2, f1]));
38
+ assert.equal(x, y, '文件数组顺序无关');
39
+ const z = hashSection(mk([{ ...f1, data: new TextEncoder().encode('# A changed\n') }, f2]));
40
+ assert.notEqual(x, z, '文件内容变化 hash 必须变化');
41
+ // contentHash 字段不参与(以字节为准)
42
+ const w = hashSection(mk([{ relativePath: f1.relativePath, data: f1.data, contentHash: 'trusted' }, f2]));
43
+ assert.equal(x, w, 'contentHash 字段值不影响分区 hash');
44
+ });
45
+
46
+ test('hashSection: JSON 分区与文件类分区 hash 空间不冲突', () => {
47
+ const jsonHash = hashSection({ version: 1, namespaces: {} } as SectionData);
48
+ const fileHash = hashSection({ version: 1, files: [] } as SectionData);
49
+ assert.notEqual(jsonHash, fileHash);
50
+ assert.equal(jsonHash.length, 64);
51
+ assert.equal(fileHash.length, 64);
52
+ });
53
+
54
+ test('saveSyncState / loadSyncState: 完整往返(含 transport 字段)', async () => {
55
+ const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'dsh-sync-state-'));
56
+ try {
57
+ const state: SyncState = {
58
+ schemaVersion: SYNC_STATE_SCHEMA_VERSION,
59
+ lastSyncAt: '2026-08-16T12:00:00.000Z',
60
+ sections: {
61
+ settings: { hash: 'a'.repeat(64), updatedAt: '2026-08-16T12:00:00.000Z' },
62
+ skills: { hash: 'b'.repeat(64), updatedAt: '2026-08-16T12:00:00.000Z' },
63
+ },
64
+ transport: { type: 'git', ref: 'main' },
65
+ lastSnapshotId: 'sync-abc',
66
+ };
67
+ await saveSyncState(tmp, state);
68
+ const raw = JSON.parse(await fs.readFile(path.join(tmp, SYNC_STATE_FILE), 'utf8'));
69
+ assert.equal(raw.schemaVersion, SYNC_STATE_SCHEMA_VERSION);
70
+ assert.deepEqual(await loadSyncState(tmp), state);
71
+ } finally {
72
+ await fs.rm(tmp, { recursive: true, force: true });
73
+ }
74
+ });
75
+
76
+ test('loadSyncState: 文件不存在 → 返回缺省空状态(lastSyncAt 为空串,lastSnapshotId 为空串)', async () => {
77
+ const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'dsh-sync-state-missing-'));
78
+ try {
79
+ const state = await loadSyncState(tmp);
80
+ assert.equal(state.schemaVersion, SYNC_STATE_SCHEMA_VERSION);
81
+ assert.equal(state.lastSyncAt, '');
82
+ assert.equal(state.lastSnapshotId, '');
83
+ assert.deepEqual(state.sections, {});
84
+ assert.equal(state.transport, undefined);
85
+ } finally {
86
+ await fs.rm(tmp, { recursive: true, force: true });
87
+ }
88
+ });
89
+
90
+ test('loadSyncState: 不支持的 schemaVersion → 拒绝', async () => {
91
+ const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'dsh-sync-state-ver-'));
92
+ try {
93
+ await fs.writeFile(path.join(tmp, SYNC_STATE_FILE), JSON.stringify({ schemaVersion: 99, lastSyncAt: '', sections: {} }), 'utf8');
94
+ await assert.rejects(() => loadSyncState(tmp), /schemaVersion/);
95
+ } finally {
96
+ await fs.rm(tmp, { recursive: true, force: true });
97
+ }
98
+ });
99
+
100
+ test('loadSyncState: 损坏 JSON → 拒绝(不静默降级)', async () => {
101
+ const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'dsh-sync-state-bad-'));
102
+ try {
103
+ await fs.writeFile(path.join(tmp, SYNC_STATE_FILE), '{not-json', 'utf8');
104
+ await assert.rejects(() => loadSyncState(tmp));
105
+ } finally {
106
+ await fs.rm(tmp, { recursive: true, force: true });
107
+ }
108
+ });
109
+
110
+ test('saveSyncState: 自动创建目录', async () => {
111
+ const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'dsh-sync-state-mkdir-'));
112
+ try {
113
+ const dir = path.join(tmp, 'nested', 'sync');
114
+ await saveSyncState(dir, { schemaVersion: SYNC_STATE_SCHEMA_VERSION, lastSyncAt: '', sections: {}, lastSnapshotId: '' });
115
+ const state = await loadSyncState(dir);
116
+ assert.equal(state.schemaVersion, SYNC_STATE_SCHEMA_VERSION);
117
+ } finally {
118
+ await fs.rm(tmp, { recursive: true, force: true });
119
+ }
120
+ });
121
+
122
+ test('loadSyncState: v1 文件 → 内存迁移到 v2 形态(lastSnapshotId=""),不动磁盘', async () => {
123
+ const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'dsh-sync-state-v1-'));
124
+ try {
125
+ // 写入一个 schemaVersion=1 的旧文件(缺 lastSnapshotId)
126
+ await fs.writeFile(
127
+ path.join(tmp, SYNC_STATE_FILE),
128
+ JSON.stringify({
129
+ schemaVersion: 1,
130
+ lastSyncAt: '2026-08-15T00:00:00.000Z',
131
+ sections: { settings: { hash: 'c'.repeat(64), updatedAt: '2026-08-15T00:00:00.000Z' } },
132
+ }),
133
+ 'utf8',
134
+ );
135
+ const state = await loadSyncState(tmp);
136
+ assert.equal(state.schemaVersion, SYNC_STATE_SCHEMA_VERSION, '内存中应已升级为 v2');
137
+ assert.equal(state.lastSnapshotId, '', 'v1 文件缺祖先指针时迁移为 ""');
138
+ assert.equal(state.lastSyncAt, '2026-08-15T00:00:00.000Z');
139
+ assert.equal(state.sections.settings?.hash, 'c'.repeat(64));
140
+ // 磁盘上仍是 v1(不就地升级)
141
+ const onDisk = JSON.parse(await fs.readFile(path.join(tmp, SYNC_STATE_FILE), 'utf8'));
142
+ assert.equal(onDisk.schemaVersion, 1);
143
+ } finally {
144
+ await fs.rm(tmp, { recursive: true, force: true });
145
+ }
146
+ });
147
+
148
+ test('saveSyncState / loadSyncState: lastSnapshotId 非空往返', async () => {
149
+ const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'dsh-sync-state-ancestor-'));
150
+ try {
151
+ const state: SyncState = {
152
+ schemaVersion: SYNC_STATE_SCHEMA_VERSION,
153
+ lastSyncAt: '2026-08-16T12:00:00.000Z',
154
+ sections: {},
155
+ lastSnapshotId: 'sync-deadbeef-1234',
156
+ };
157
+ await saveSyncState(tmp, state);
158
+ const loaded = await loadSyncState(tmp);
159
+ assert.equal(loaded.lastSnapshotId, 'sync-deadbeef-1234');
160
+ assert.deepEqual(loaded, state);
161
+ } finally {
162
+ await fs.rm(tmp, { recursive: true, force: true });
163
+ }
164
+ });
@@ -0,0 +1,131 @@
1
+ /**
2
+ * m-sync-transport:sync-state.json 模型。
3
+ * 记录上次同步时间与各分区内容 hash,供变更检测(与远端快照对比决定是否重传)。
4
+ * 纯逻辑 + 可注入 fs;不触碰真实 ~/.dsh。
5
+ */
6
+ import { sha256Hex } from '../utils/hashing.ts';
7
+ import { parseJsonSafe, stringifyJsonSafe } from '../utils/json.ts';
8
+ import { zhMsg } from '../core/messages.ts';
9
+ import type { MsgFunc } from '../core/messages.ts';
10
+ import type { FilesSection, SectionData, SectionId } from '../schema/types.ts';
11
+ import { createSnapshotFs, joinFs } from './fs.ts';
12
+ import type { SnapshotFs } from './fs.ts';
13
+
14
+ export const SYNC_STATE_SCHEMA_VERSION = 2;
15
+ /** 历史可读取版本:v1 缺 lastSnapshotId 时做内存迁移到 v2(lastSnapshotId='')。 */
16
+ export const SYNC_STATE_SUPPORTED_VERSIONS: readonly number[] = [1, 2];
17
+ export const SYNC_STATE_FILE = 'sync-state.json';
18
+
19
+ /** 单个分区的同步状态:内容 hash + 最近变更时间 */
20
+ export interface SyncSectionState {
21
+ hash: string;
22
+ updatedAt: string; // ISO-8601 UTC
23
+ }
24
+
25
+ /** sync-state.json 结构 */
26
+ export interface SyncState {
27
+ schemaVersion: number;
28
+ lastSyncAt: string; // ISO-8601 UTC;'' = 从未同步
29
+ sections: Partial<Record<SectionId, SyncSectionState>>;
30
+ /** 当前绑定的传输通道(type = SyncTransport.type,ref = 通道内引用,如 git 分支) */
31
+ transport?: { type: string; ref: string };
32
+ /** 最近一次同步的快照 ID(= 共同祖先指针);'' = 从未同步或无祖先 */
33
+ lastSnapshotId: string;
34
+ }
35
+
36
+ /** 文件类分区判定:{ version: 1, files: [...] }(duck-typing,与 JSON 分区区分) */
37
+ function isFilesSection(data: SectionData): data is FilesSection {
38
+ const obj = data as { version?: unknown; files?: unknown };
39
+ return data !== null && typeof data === 'object' && obj.version === 1 && Array.isArray(obj.files);
40
+ }
41
+
42
+ /** 键序规范化的 JSON 序列化(跨机器/跨解析稳定,用于内容 hash) */
43
+ function canonicalJson(value: unknown): string {
44
+ if (value === null || typeof value !== 'object') {
45
+ if (value === undefined) return 'null'; // 防御:不产生 undefined 字面量
46
+ return JSON.stringify(value);
47
+ }
48
+ if (Array.isArray(value)) {
49
+ return `[${value.map((v) => canonicalJson(v)).join(',')}]`;
50
+ }
51
+ const obj = value as Record<string, unknown>;
52
+ const parts: string[] = [];
53
+ for (const key of Object.keys(obj).sort()) {
54
+ const v = obj[key];
55
+ if (v === undefined) continue; // 与 JSON.stringify 一致:跳过 undefined 键
56
+ parts.push(`${JSON.stringify(key)}:${canonicalJson(v)}`);
57
+ }
58
+ return `{${parts.join(',')}}`;
59
+ }
60
+
61
+ /**
62
+ * 分区内容 hash:
63
+ * - JSON 分区:键序规范化的 JSON 序列化 → SHA-256(同一内容不同键序 → 相同 hash)
64
+ * - 文件类分区:文件相对路径 + 文件字节 SHA-256 的有序清单 → SHA-256
65
+ * (文件数组顺序无关;contentHash 字段不参与,以字节为准)
66
+ */
67
+ export function hashSection(data: SectionData): string {
68
+ if (isFilesSection(data)) {
69
+ const files = [...data.files]
70
+ .sort((a, b) => (a.relativePath < b.relativePath ? -1 : a.relativePath > b.relativePath ? 1 : 0))
71
+ .map((f) => `${f.relativePath}\u0000${sha256Hex(f.data)}`)
72
+ .join('\u0001');
73
+ return sha256Hex(`files:v1\u0000${files}`);
74
+ }
75
+ return sha256Hex(canonicalJson(data));
76
+ }
77
+
78
+ /** 读取同步状态;文件不存在 → 返回缺省空状态(从未同步)。
79
+ * v1 文件 → 内存迁移到 v2(lastSnapshotId=''),不在磁盘上就地升级。 */
80
+ export async function loadSyncState(dir: string, fsx: SnapshotFs = createSnapshotFs(), msg: MsgFunc = zhMsg): Promise<SyncState> {
81
+ const file = joinFs(dir, SYNC_STATE_FILE);
82
+ if (!(await fsx.exists(file))) {
83
+ return { schemaVersion: SYNC_STATE_SCHEMA_VERSION, lastSyncAt: '', sections: {}, lastSnapshotId: '' };
84
+ }
85
+ const raw = Buffer.from(await fsx.readFile(file)).toString('utf8');
86
+ const parsed = parseJsonSafe(raw);
87
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
88
+ throw new Error(msg('sync.state.notObject'));
89
+ }
90
+ const obj = parsed as Record<string, unknown>;
91
+ if (typeof obj['schemaVersion'] !== 'number' || !SYNC_STATE_SUPPORTED_VERSIONS.includes(obj['schemaVersion'])) {
92
+ throw new Error(msg('sync.state.schemaUnsupported', { version: String(obj['schemaVersion']), expected: String(SYNC_STATE_SUPPORTED_VERSIONS.join('/')) }));
93
+ }
94
+ if (typeof obj['lastSyncAt'] !== 'string') {
95
+ throw new Error(msg('sync.state.lastSyncAt'));
96
+ }
97
+ if (obj['sections'] === null || typeof obj['sections'] !== 'object' || Array.isArray(obj['sections'])) {
98
+ throw new Error(msg('sync.state.sections'));
99
+ }
100
+ const sections: SyncState['sections'] = {};
101
+ for (const [sid, rec] of Object.entries(obj['sections'] as Record<string, unknown>)) {
102
+ if (rec === null || typeof rec !== 'object' || Array.isArray(rec)) {
103
+ throw new Error(msg('sync.state.sectionRecord', { section: sid }));
104
+ }
105
+ const r = rec as Record<string, unknown>;
106
+ if (typeof r['hash'] !== 'string' || typeof r['updatedAt'] !== 'string') {
107
+ throw new Error(msg('sync.state.sectionFields', { section: sid }));
108
+ }
109
+ sections[sid as SectionId] = { hash: r['hash'], updatedAt: r['updatedAt'] };
110
+ }
111
+ const state: SyncState = {
112
+ schemaVersion: SYNC_STATE_SCHEMA_VERSION,
113
+ lastSyncAt: obj['lastSyncAt'],
114
+ sections,
115
+ lastSnapshotId: typeof obj['lastSnapshotId'] === 'string' ? obj['lastSnapshotId'] : '',
116
+ };
117
+ const t = obj['transport'];
118
+ if (t !== undefined) {
119
+ if (t === null || typeof t !== 'object' || typeof (t as Record<string, unknown>)['type'] !== 'string' || typeof (t as Record<string, unknown>)['ref'] !== 'string') {
120
+ throw new Error(msg('sync.state.transport'));
121
+ }
122
+ state.transport = { type: (t as { type: string }).type, ref: (t as { ref: string }).ref };
123
+ }
124
+ return state;
125
+ }
126
+
127
+ /** 保存同步状态(自动创建目录) */
128
+ export async function saveSyncState(dir: string, state: SyncState, fsx: SnapshotFs = createSnapshotFs()): Promise<void> {
129
+ await fsx.mkdir(dir);
130
+ await fsx.writeFile(joinFs(dir, SYNC_STATE_FILE), new TextEncoder().encode(stringifyJsonSafe(state, { space: 2 })));
131
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * m-sync-transport:SyncTransport 抽象层测试。
3
+ * - computeSnapshotMeta:sections hash 记录 + manifest 摘要透传
4
+ * - manifestSummaryFrom:Manifest → 摘要
5
+ * - 接口形状由类型系统保证(编译期),此处验证纯 helper 行为。
6
+ */
7
+ import test from 'node:test';
8
+ import assert from 'node:assert/strict';
9
+
10
+ import { computeSnapshotMeta, manifestSummaryFrom } from './transport.ts';
11
+ import { hashSection } from './sync-state.ts';
12
+ import type { ManifestSummary, SyncSnapshot, SyncTransport } from './transport.ts';
13
+ import type { Manifest, SectionData, SectionId } from '../schema/types.ts';
14
+
15
+ function sampleSnapshot(overrides: Partial<SyncSnapshot> = {}): SyncSnapshot {
16
+ return {
17
+ id: 'snap-001',
18
+ createdAt: '2026-08-16T12:00:00.000Z',
19
+ manifest: {
20
+ schemaVersion: 1,
21
+ dshVersion: '1.2.3',
22
+ platform: 'win32',
23
+ sectionIds: ['settings', 'providers'],
24
+ containsSecrets: false,
25
+ },
26
+ sections: {
27
+ settings: { version: 1, namespaces: { general: { value: { theme: 'dark' }, revision: 1, secrets: [] } } },
28
+ providers: { version: 1, providers: { deepseek: { route: '/v1' } } },
29
+ },
30
+ ...overrides,
31
+ };
32
+ }
33
+
34
+ test('computeSnapshotMeta: 透传 id/createdAt/manifest,sections 为各分区内容 hash', () => {
35
+ const snap = sampleSnapshot();
36
+ const meta = computeSnapshotMeta(snap);
37
+ assert.equal(meta.id, 'snap-001');
38
+ assert.equal(meta.createdAt, snap.createdAt);
39
+ assert.deepEqual(meta.manifest, snap.manifest);
40
+ const snapPlain = snap.sections as Partial<Record<string, unknown>>;
41
+ assert.equal(meta.sections['settings'], hashSection(snapPlain['settings'] as SectionData));
42
+ assert.equal(meta.sections['providers'], hashSection(snapPlain['providers'] as SectionData));
43
+ assert.equal(Object.keys(meta.sections).length, 2);
44
+ });
45
+
46
+ test('computeSnapshotMeta: 空 sections → 空 hash 记录', () => {
47
+ const meta = computeSnapshotMeta(sampleSnapshot({ sections: {} }));
48
+ assert.deepEqual(meta.sections, {});
49
+ });
50
+
51
+ test('computeSnapshotMeta: hash 与分区内容一一对应,内容变化 → hash 变化', () => {
52
+ const a = computeSnapshotMeta(sampleSnapshot());
53
+ const b = computeSnapshotMeta(sampleSnapshot({
54
+ sections: {
55
+ settings: { version: 1, namespaces: { general: { value: { theme: 'light' }, revision: 1, secrets: [] } } },
56
+ providers: { version: 1, providers: { deepseek: { route: '/v1' } } },
57
+ },
58
+ }));
59
+ assert.notEqual(a.sections['settings'], b.sections['settings'], 'settings 内容变化 hash 必须变化');
60
+ assert.equal(a.sections['providers'], b.sections['providers'], 'providers 未变化 hash 必须相同');
61
+ });
62
+
63
+ test('manifestSummaryFrom: 从导出 Manifest 提取摘要(只含启用分区)', () => {
64
+ const manifest: Manifest = {
65
+ schemaVersion: 1,
66
+ exporter: { name: 'dsh-config-manager', version: '0.1.0' },
67
+ source: { dshVersion: '2.0.0', platform: 'linux', arch: 'x64' },
68
+ exportedAt: '2026-08-16T12:00:00.000Z',
69
+ sections: {
70
+ settings: true, ui: true, providers: true, plugins: false, mcp: false,
71
+ prompts: false, skills: true, agentPresets: false, agentInstructions: false, workspaces: false,
72
+ pluginFiles: false, credentialsStatus: false, secrets: false, sessions: false, self: false,
73
+ },
74
+ security: { containsSecrets: true, encrypted: false, encryption: null },
75
+ };
76
+ const summary = manifestSummaryFrom(manifest);
77
+ assert.equal(summary.schemaVersion, 1);
78
+ assert.equal(summary.dshVersion, '2.0.0');
79
+ assert.equal(summary.platform, 'linux');
80
+ assert.deepEqual(summary.sectionIds, ['settings', 'ui', 'providers', 'skills']);
81
+ assert.equal(summary.containsSecrets, true);
82
+ });
83
+
84
+ test('SyncTransport 契约形状:实现对象必须具备 list/upload/download/delete', async () => {
85
+ const fake: SyncTransport = {
86
+ type: 'memory',
87
+ async list() { return []; },
88
+ async upload(s) { return computeSnapshotMeta(s); },
89
+ async download(id: string) { return sampleSnapshot({ id }); },
90
+ async delete(_id: string) {},
91
+ };
92
+ const snap = sampleSnapshot();
93
+ const meta = computeSnapshotMeta(snap);
94
+ assert.equal(fake.type, 'memory');
95
+ assert.equal((await fake.list()).length, 0);
96
+ assert.equal((await fake.download('x')).id, 'x');
97
+ assert.equal((await fake.upload(snap)).id, meta.id);
98
+ assert.doesNotThrow(() => fake.delete('x'));
99
+ });
100
+
101
+ test('ManifestSummary 类型形状(编译期保证 + 运行期检查)', () => {
102
+ const s: ManifestSummary = {
103
+ schemaVersion: 1,
104
+ dshVersion: '1.0.0',
105
+ platform: 'darwin',
106
+ sectionIds: [] as SectionId[],
107
+ containsSecrets: false,
108
+ };
109
+ assert.equal(s.schemaVersion, 1);
110
+ assert.ok(Array.isArray(s.sectionIds));
111
+ });