@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,424 @@
1
+ /**
2
+ * 配置状态快照(Phase 1 P0-1 / P0-2 的持久化与回放层)。
3
+ *
4
+ * 与既有 `core/backup.ts` 的 `Snapshot` 的分工:
5
+ * - `Snapshot`(backup.ts):**导入计划驱动**,只登记「本次导入将写入的目标」的原值,
6
+ * 用于导入失败回滚。它不描述「整体配置在某时刻是什么样」。
7
+ * - `ConfigSnapshot`(本模块):**状态驱动**,记录整份配置在某一时刻的分区数据 + 分区指纹,
8
+ * 用于自动快照、撤销/重做、最后正常状态定位。恢复方式是**回放 adapter**
9
+ * (validate → analyzeImport → applyItem,与导入/Profile 切换同一条管线),
10
+ * 因此天然覆盖 settings 命名空间、patch 行、文件类分区与插件安装。
11
+ *
12
+ * 存储布局:`<dir>/<id>/config-snapshot.json` = `{ meta, data }`。
13
+ * `data` 为各分区的 ExportSection.data;其中的 Uint8Array(文件类分区的字节)
14
+ * 经 `__u8` base64 包封做可逆 JSON 编码(JSON.stringify 会把字节变成 {0:..,1:..})。
15
+ *
16
+ * 安全:快照 id 严格白名单校验(防穿越);写入走 atomicWriteFile;分区数据只含
17
+ * `includeSecrets: false` 的导出结果,密钥值从不进入本模块。
18
+ */
19
+ import fs from 'node:fs/promises';
20
+ import path from 'node:path';
21
+ import { atomicWriteFile } from '../utils/atomic-write.js';
22
+ import { parseJsonSafe } from '../utils/json.js';
23
+ import { SECTION_IDS } from '../schema/config.js';
24
+ import { CURRENT_SCHEMA_VERSION } from '../schema/versions.js';
25
+ import { msgOf, zhMsg } from './messages.js';
26
+ import { isValidSnapshotId } from './restore.js';
27
+ import { statesEqual } from './config-state.js';
28
+ /** 快照文件名(每个快照一个目录) */
29
+ export const CONFIG_SNAPSHOT_FILE = 'config-snapshot.json';
30
+ /** 可逆 JSON 编码:Uint8Array → { __u8: base64 } */
31
+ const U8_KEY = '__u8';
32
+ function encodeForJson(value) {
33
+ if (value instanceof Uint8Array)
34
+ return { [U8_KEY]: Buffer.from(value).toString('base64') };
35
+ if (Array.isArray(value))
36
+ return value.map(encodeForJson);
37
+ if (value !== null && typeof value === 'object') {
38
+ const out = {};
39
+ for (const [k, v] of Object.entries(value))
40
+ out[k] = encodeForJson(v);
41
+ return out;
42
+ }
43
+ return value;
44
+ }
45
+ function decodeFromJson(value) {
46
+ if (Array.isArray(value))
47
+ return value.map(decodeFromJson);
48
+ if (value !== null && typeof value === 'object') {
49
+ const rec = value;
50
+ const keys = Object.keys(rec);
51
+ if (keys.length === 1 && keys[0] === U8_KEY && typeof rec[U8_KEY] === 'string') {
52
+ return new Uint8Array(Buffer.from(rec[U8_KEY], 'base64'));
53
+ }
54
+ const out = {};
55
+ for (const [k, v] of Object.entries(rec))
56
+ out[k] = decodeFromJson(v);
57
+ return out;
58
+ }
59
+ return value;
60
+ }
61
+ /** 供 undo.ts 使用的最小候选视图 */
62
+ export function toUndoCandidate(meta) {
63
+ return {
64
+ id: meta.id,
65
+ createdAt: meta.createdAt,
66
+ kind: meta.kind,
67
+ state: meta.state,
68
+ consumed: meta.consumed,
69
+ };
70
+ }
71
+ /* ------------------------------------------------------------ 读写 */
72
+ function snapshotDir(dir, id) {
73
+ if (!isValidSnapshotId(id))
74
+ throw new Error(`非法配置快照 id: ${JSON.stringify(id)}`);
75
+ const target = path.join(dir, id);
76
+ if (!target.startsWith(path.resolve(dir) + path.sep))
77
+ throw new Error(`配置快照路径越界: ${id}`);
78
+ return target;
79
+ }
80
+ /** 生成快照 id:时间戳 + 随机短串(可读 + 唯一;与既有 uuid 风格区分开) */
81
+ export function makeConfigSnapshotId(now = () => new Date()) {
82
+ const d = now();
83
+ const p = (n, w = 2) => String(n).padStart(w, '0');
84
+ const stamp = `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
85
+ const rand = Math.random().toString(36).slice(2, 6).padEnd(4, '0');
86
+ return `${stamp}-${rand}`;
87
+ }
88
+ /**
89
+ * 写入一个配置快照。返回落盘后的元数据。
90
+ * id 冲突时自动追加后缀重试(同一秒内多次快照)。
91
+ */
92
+ export async function saveConfigSnapshot(opts) {
93
+ const { dir, kind, reason, sections, state } = opts;
94
+ const now = opts.now ?? (() => new Date());
95
+ const idFactory = opts.idFactory ?? (() => makeConfigSnapshotId(now));
96
+ const maxBytes = opts.maxBytes ?? 64 * 1024 * 1024;
97
+ await fs.mkdir(dir, { recursive: true });
98
+ let id = idFactory();
99
+ for (let attempt = 0; attempt < 50; attempt++) {
100
+ try {
101
+ await fs.access(path.join(dir, id));
102
+ id = `${idFactory()}-${Math.random().toString(36).slice(2, 5)}`;
103
+ }
104
+ catch {
105
+ break; // 不存在 → 可用
106
+ }
107
+ }
108
+ const data = {};
109
+ for (const [sectionId, exported] of sections) {
110
+ data[sectionId] = encodeForJson(exported.data);
111
+ }
112
+ const sectionsList = [...sections.keys()].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
113
+ const payload = { meta: null, data };
114
+ const metaBase = {
115
+ id,
116
+ createdAt: now().toISOString(),
117
+ kind,
118
+ reason,
119
+ ...(opts.trigger !== undefined ? { trigger: opts.trigger } : {}),
120
+ sections: sectionsList,
121
+ state,
122
+ ...(opts.note !== undefined ? { note: opts.note } : {}),
123
+ ...(opts.tags !== undefined ? { tags: opts.tags } : {}),
124
+ };
125
+ payload.meta = metaBase;
126
+ const serialized = JSON.stringify(payload, null, 2);
127
+ const bytes = Buffer.byteLength(serialized);
128
+ if (bytes > maxBytes) {
129
+ throw new Error(`配置快照超出上限(${bytes} > ${maxBytes} 字节);请缩小分区范围后重试`);
130
+ }
131
+ const meta = { ...metaBase, totalBytes: bytes };
132
+ payload.meta = meta;
133
+ const finalText = JSON.stringify(payload, null, 2);
134
+ const target = snapshotDir(dir, id);
135
+ await fs.mkdir(target, { recursive: true });
136
+ await atomicWriteFile(path.join(target, CONFIG_SNAPSHOT_FILE), finalText);
137
+ return meta;
138
+ }
139
+ /**
140
+ * meta.state 形状校验。
141
+ * 语义:
142
+ * - 缺失 / undefined → **放行**(旧格式快照,撤销侧会按「无法比对」剔除,
143
+ * 但它仍应出现在列表里可见/可恢复);
144
+ * - 存在但不是 `{ sections: [...] }` → 判为损坏并整条丢弃:继续信任它会让
145
+ * statesEqual/planUndo 拿到形状不对的对象(历史上会直接抛 TypeError,
146
+ * 一条坏文件就能让整个灾备面板 500)。
147
+ */
148
+ function isValidStateField(value) {
149
+ if (value === undefined)
150
+ return true;
151
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
152
+ return false;
153
+ return Array.isArray(value.sections);
154
+ }
155
+ /** 读单个快照的元数据;缺失/损坏/非法 id → null(绝不抛)。 */
156
+ export async function readConfigSnapshotMeta(dir, id) {
157
+ if (!isValidSnapshotId(id))
158
+ return null;
159
+ try {
160
+ const raw = await fs.readFile(path.join(dir, id, CONFIG_SNAPSHOT_FILE), 'utf8');
161
+ const parsed = parseJsonSafe(raw);
162
+ const meta = parsed?.meta;
163
+ if (meta === null || meta === undefined || typeof meta !== 'object')
164
+ return null;
165
+ if (meta.id !== id || typeof meta.createdAt !== 'string' || typeof meta.kind !== 'string')
166
+ return null;
167
+ if (!isValidStateField(meta.state))
168
+ return null;
169
+ return meta;
170
+ }
171
+ catch {
172
+ return null;
173
+ }
174
+ }
175
+ /** 列出全部快照元数据(按 createdAt 倒序;损坏条目跳过,不阻断其余)。 */
176
+ export async function listConfigSnapshots(dir) {
177
+ let entries;
178
+ try {
179
+ entries = await fs.readdir(dir, { withFileTypes: true });
180
+ }
181
+ catch {
182
+ return [];
183
+ }
184
+ const metas = [];
185
+ for (const entry of entries) {
186
+ if (!entry.isDirectory())
187
+ continue;
188
+ const meta = await readConfigSnapshotMeta(dir, entry.name);
189
+ if (meta !== null)
190
+ metas.push(meta);
191
+ }
192
+ metas.sort((a, b) => {
193
+ if (a.createdAt !== b.createdAt)
194
+ return a.createdAt < b.createdAt ? 1 : -1;
195
+ return a.id < b.id ? 1 : a.id > b.id ? -1 : 0;
196
+ });
197
+ return metas;
198
+ }
199
+ /** 读完整快照(含分区数据)。缺失/损坏抛出。 */
200
+ export async function loadConfigSnapshot(dir, id) {
201
+ const target = snapshotDir(dir, id);
202
+ const raw = await fs.readFile(path.join(target, CONFIG_SNAPSHOT_FILE), 'utf8');
203
+ const parsed = parseJsonSafe(raw);
204
+ const meta = parsed?.meta;
205
+ if (meta === null || meta === undefined || typeof meta !== 'object' || meta.id !== id) {
206
+ throw new Error(`配置快照 ${id} 的 ${CONFIG_SNAPSHOT_FILE} 不是合法对象`);
207
+ }
208
+ const data = {};
209
+ for (const [k, v] of Object.entries(parsed?.data ?? {})) {
210
+ data[k] = decodeFromJson(v);
211
+ }
212
+ return { ...meta, data };
213
+ }
214
+ /** 原地更新元数据字段;快照不存在返回 null。 */
215
+ export async function updateConfigSnapshotMeta(dir, id, patch) {
216
+ if (!isValidSnapshotId(id))
217
+ return null;
218
+ const target = path.join(dir, id, CONFIG_SNAPSHOT_FILE);
219
+ let raw;
220
+ try {
221
+ raw = await fs.readFile(target, 'utf8');
222
+ }
223
+ catch {
224
+ return null;
225
+ }
226
+ const parsed = parseJsonSafe(raw);
227
+ if (parsed?.meta === null || parsed?.meta === undefined)
228
+ return null;
229
+ const meta = { ...parsed.meta };
230
+ for (const [k, v] of Object.entries(patch)) {
231
+ if (v === undefined)
232
+ continue;
233
+ meta[k] = v;
234
+ }
235
+ await atomicWriteFile(target, JSON.stringify({ meta, data: parsed.data ?? {} }, null, 2));
236
+ return meta;
237
+ }
238
+ /** 删除单个快照(幂等:不存在视为成功)。 */
239
+ export async function deleteConfigSnapshot(dir, id) {
240
+ if (!isValidSnapshotId(id))
241
+ throw new Error(`非法配置快照 id: ${JSON.stringify(id)}`);
242
+ const target = snapshotDir(dir, id);
243
+ try {
244
+ await fs.stat(target);
245
+ }
246
+ catch {
247
+ return false;
248
+ }
249
+ await fs.rm(target, { recursive: true, force: true });
250
+ return true;
251
+ }
252
+ /**
253
+ * 保留清理:按 kind 分桶,各自保留最新 N 份,删除更旧的。
254
+ * pinned 快照**一律豁免**(用户显式保留的回滚点不得被自动淘汰)。
255
+ * 返回被删除的快照 id 列表。
256
+ */
257
+ export async function pruneConfigSnapshots(dir, opts = {}) {
258
+ const keepAuto = opts.keepAuto ?? 20;
259
+ const keepPre = opts.keepPreRestore ?? 10;
260
+ const keepManual = opts.keepManual ?? Number.POSITIVE_INFINITY;
261
+ const metas = (await listConfigSnapshots(dir)).filter((m) => m.pinned !== true);
262
+ const bucket = (kind) => {
263
+ if (kind === 'pre-restore')
264
+ return keepPre;
265
+ if (kind === 'manual')
266
+ return keepManual;
267
+ return keepAuto;
268
+ };
269
+ const grouped = new Map();
270
+ for (const meta of metas) {
271
+ const limit = bucket(meta.kind);
272
+ const list = grouped.get(limit) ?? [];
273
+ list.push(meta);
274
+ grouped.set(limit, list);
275
+ }
276
+ // metas 已按 createdAt 倒序 → 每桶内前 limit 个保留,其余删除
277
+ const removed = [];
278
+ for (const [limit, list] of grouped) {
279
+ if (!Number.isFinite(limit))
280
+ continue;
281
+ for (const meta of list.slice(Math.max(0, limit))) {
282
+ await fs.rm(path.join(dir, meta.id), { recursive: true, force: true });
283
+ removed.push(meta.id);
284
+ }
285
+ }
286
+ return removed;
287
+ }
288
+ /* ------------------------------------------------------------ 回放恢复 */
289
+ /** 构建回放用 manifest(与 profiles/profile-manager.ts 的 buildProfileManifest 同构) */
290
+ function buildReplayManifest(ctx) {
291
+ const sections = {};
292
+ for (const id of SECTION_IDS)
293
+ sections[id] = false;
294
+ return {
295
+ schemaVersion: CURRENT_SCHEMA_VERSION,
296
+ exporter: { name: 'DSH Config Manager', version: '0.1.0' },
297
+ source: {
298
+ dshVersion: ctx.dshVersion,
299
+ platform: ctx.platform,
300
+ arch: ctx.arch,
301
+ },
302
+ exportedAt: new Date().toISOString(),
303
+ sections: sections,
304
+ security: { containsSecrets: false, encrypted: false, encryption: null },
305
+ };
306
+ }
307
+ /** 非破坏性计划项:不调用 applyItem(与导入管线的「可执行 kinds」口径一致) */
308
+ const NON_EXECUTABLE_KINDS = new Set(['Skip', 'Warning', 'MissingSecret', 'MissingDependency', 'PathMapping']);
309
+ /**
310
+ * 回放一个配置快照:对每个分区 validate → analyzeImport → applyItem。
311
+ * 与导入/Profile 切换共用 adapter 管线,因此行为一致、无第二套写入逻辑。
312
+ * 单项失败不拖垮其余(如实计入 failed),与 rollback.ts 的尽力语义一致。
313
+ */
314
+ export async function restoreConfigSnapshot(opts) {
315
+ const { dir, id, adapters, ctx } = opts;
316
+ const msg = opts.msg ?? ctx.msg ?? zhMsg;
317
+ const snapshot = await loadConfigSnapshot(dir, id);
318
+ const only = opts.only !== undefined ? new Set(opts.only) : null;
319
+ const sections = new Map();
320
+ for (const [k, v] of Object.entries(snapshot.data)) {
321
+ if (only !== null && !only.has(k))
322
+ continue;
323
+ sections.set(k, v);
324
+ }
325
+ const importCtx = {
326
+ manifest: buildReplayManifest(ctx),
327
+ targetPlatform: ctx.platform,
328
+ target: ctx,
329
+ sections,
330
+ pathMappings: [],
331
+ resolutions: {},
332
+ secretInputs: {},
333
+ log: ctx.log,
334
+ msg,
335
+ };
336
+ const ordered = opts.applyOrder !== undefined
337
+ ? [...adapters].sort((a, b) => {
338
+ const ia = opts.applyOrder.indexOf(a.id);
339
+ const ib = opts.applyOrder.indexOf(b.id);
340
+ return (ia < 0 ? Number.MAX_SAFE_INTEGER : ia) - (ib < 0 ? Number.MAX_SAFE_INTEGER : ib);
341
+ })
342
+ : [...adapters];
343
+ const report = {
344
+ ok: true,
345
+ snapshotId: id,
346
+ applied: [],
347
+ skipped: [],
348
+ failed: [],
349
+ invalidSections: [],
350
+ needsRestart: false,
351
+ };
352
+ for (const adapter of ordered) {
353
+ const data = sections.get(adapter.id);
354
+ if (data === undefined)
355
+ continue;
356
+ let items;
357
+ try {
358
+ const validation = await adapter.validate(data, msg);
359
+ if (!validation.valid) {
360
+ const reason = validation.issues
361
+ .filter((i) => i.severity === 'error')
362
+ .map((i) => `${i.path}: ${i.message}`)
363
+ .join('; ');
364
+ report.invalidSections.push({ section: adapter.id, reason: reason === '' ? 'validation failed' : reason });
365
+ continue;
366
+ }
367
+ items = await adapter.analyzeImport(data, importCtx);
368
+ }
369
+ catch (err) {
370
+ report.invalidSections.push({
371
+ section: adapter.id,
372
+ reason: err instanceof Error ? err.message : String(err),
373
+ });
374
+ continue;
375
+ }
376
+ let index = 0;
377
+ for (const item of items) {
378
+ index += 1;
379
+ opts.onItem?.({ index, total: items.length, detail: `${adapter.id}:${item.id}` });
380
+ if (item.kind === 'Error') {
381
+ report.failed.push({ item: `${adapter.id}:${item.id}`, reason: item.description });
382
+ report.ok = false;
383
+ continue;
384
+ }
385
+ if (NON_EXECUTABLE_KINDS.has(item.kind)) {
386
+ report.skipped.push(`${adapter.id}:${item.id}`);
387
+ continue;
388
+ }
389
+ try {
390
+ const result = await adapter.applyItem(item, importCtx);
391
+ if (result.ok) {
392
+ report.applied.push(`${adapter.id}:${item.id}`);
393
+ if (result.needsRestart === true)
394
+ report.needsRestart = true;
395
+ }
396
+ else if (result.warning === true) {
397
+ report.skipped.push(`${adapter.id}:${item.id}`);
398
+ }
399
+ else {
400
+ report.failed.push({
401
+ item: `${adapter.id}:${item.id}`,
402
+ reason: result.message ?? 'applyItem 返回失败',
403
+ });
404
+ report.ok = false;
405
+ }
406
+ }
407
+ catch (err) {
408
+ report.failed.push({
409
+ item: `${adapter.id}:${item.id}`,
410
+ reason: err instanceof Error ? err.message : String(err),
411
+ });
412
+ report.ok = false;
413
+ }
414
+ }
415
+ }
416
+ return report;
417
+ }
418
+ /** 便捷:两个快照的分区指纹是否等价(供 UI「与当前一致」提示) */
419
+ export function snapshotsEquivalent(a, b) {
420
+ return statesEqual(a.state, b.state);
421
+ }
422
+ /** 供调用方取翻译器(保持与 core 其他模块一致的导出面) */
423
+ export const configSnapshotMsgOf = msgOf;
424
+ //# sourceMappingURL=config-snapshot.js.map
@@ -0,0 +1,70 @@
1
+ import type { SectionId } from '../schema/types.ts';
2
+ import type { ConfigAdapter, ExportSection, HostContext } from './types.ts';
3
+ /** 单分区状态:内容指纹 + 计数(counts 仅展示,不参与相等性判定) */
4
+ export interface SectionState {
5
+ section: SectionId;
6
+ /** 分区内容的稳定 sha256(hex) */
7
+ hash: string;
8
+ /** adapter 自报计数(展示用;不参与相等判定) */
9
+ counts: Record<string, number>;
10
+ /** 文件类分区的文件条数(非文件分区为 0) */
11
+ fileCount: number;
12
+ }
13
+ /** 一次配置状态采集结果 */
14
+ export interface ConfigState {
15
+ /** ISO 时间戳 */
16
+ capturedAt: string;
17
+ /** 按 section 名升序,保证 stateSections/diff 的确定性 */
18
+ sections: SectionState[];
19
+ }
20
+ /**
21
+ * 稳定序列化:对象键**排序**后输出,保证同一逻辑内容得到同一字符串。
22
+ * 处理 undefined / 函数 → 省略(与 JSON 语义一致);Date → ISO;Uint8Array → base64 标记。
23
+ * 不追求通用性:只服务本模块的哈希输入(adapter 导出数据 = JSON 安全结构)。
24
+ */
25
+ export declare function stableStringify(value: unknown): string;
26
+ /**
27
+ * 分区内容指纹。
28
+ * data 走 stableStringify;files(文件类分区)按「相对路径 + 内容 sha256」参与,
29
+ * 使文件重命名/内容变化都能被识别,而不必把字节写进哈希输入。
30
+ */
31
+ export declare function hashExportSection(section: Pick<ExportSection, 'data' | 'files'>): string;
32
+ export interface CaptureConfigStateOptions {
33
+ /** 仅采集这些分区(缺省 = 全部传入的 adapter) */
34
+ only?: readonly SectionId[];
35
+ /** 单分区采集失败回调(该分区被跳过,不进入 state) */
36
+ onSectionError?: (section: SectionId, error: unknown) => void;
37
+ }
38
+ /**
39
+ * 由**已导出的**分区结果构建状态(按 section 升序)。
40
+ *
41
+ * 单一事实源:`captureConfigState` 与「先导出再落快照」的快照路径都必须走这里,
42
+ * 否则两条路径的分区口径(hash / counts / fileCount)会悄悄漂移。
43
+ */
44
+ export declare function stateFromExports(sections: ReadonlyMap<SectionId, ExportSection>, capturedAt?: string): ConfigState;
45
+ /**
46
+ * 采集当前配置状态。分区按 id 升序输出以保证可比性。
47
+ * 单分区失败 → 跳过(记入 onSectionError),不抛出。
48
+ */
49
+ export declare function captureConfigState(adapters: readonly ConfigAdapter[], ctx: HostContext, opts?: CaptureConfigStateOptions): Promise<ConfigState>;
50
+ /** section → SectionState(便于比较与展示);state 为 null/损坏时返回空表(不抛)。 */
51
+ export declare function stateSections(state: ConfigState | null): Map<SectionId, SectionState>;
52
+ /**
53
+ * 两个状态的内容是否完全一致(分区集合 + 每分区 hash)。
54
+ *
55
+ * 全函数:null / undefined / 缺 sections 的损坏状态一律**不抛**——
56
+ * 任一侧不可解析时退化为引用相等(null vs null 为 true,其余为 false)。
57
+ */
58
+ export declare function statesEqual(a: ConfigState | null, b: ConfigState | null): boolean;
59
+ export interface ConfigStateDiff {
60
+ /** 两侧都有但 hash 不同 */
61
+ changed: SectionId[];
62
+ /** 仅 after 有 */
63
+ added: SectionId[];
64
+ /** 仅 before 有 */
65
+ removed: SectionId[];
66
+ /** 无任何差异 */
67
+ identical: boolean;
68
+ }
69
+ /** 分区级差异(before → after)。顺序确定(按 section 升序)。 */
70
+ export declare function diffStates(before: ConfigState | null, after: ConfigState | null): ConfigStateDiff;
@@ -0,0 +1,177 @@
1
+ /**
2
+ * 配置状态向量(Phase 1 地基:P0-1 自动快照 / P0-2 撤销重做 共用)。
3
+ *
4
+ * 问题:既有 `Snapshot` 是**导入计划驱动**的(只登记本次导入将写入的目标),
5
+ * 因此它无法回答「相对于某个时刻,配置整体有没有变」「变的是哪个分区」。
6
+ * 撤销/重做与自动快照需要的正是后者。
7
+ *
8
+ * 本模块把「当前 DSH 配置」压成一个**可分区的稳定指纹集合**(ConfigState):
9
+ * - 每个分区经 `adapter.export(ctx, { includeSecrets: false })` 读取;
10
+ * - 对分区内容做**稳定序列化**后取 sha256(对象键排序,跨进程/跨机器一致);
11
+ * - 文件类分区的文件字节按 sha256 计入,不把二进制塞进哈希输入。
12
+ *
13
+ * 边界:纯计算 + 只读 adapter,零写入;不持久化(持久化见 config-snapshot.ts)。
14
+ * 采集失败的分区**跳过并回调**,绝不因单分区异常而使整次采集失败——否则一个坏
15
+ * adapter 会让自动快照静默停摆。
16
+ */
17
+ import { sha256Hex } from '../utils/hashing.js';
18
+ /**
19
+ * 稳定序列化:对象键**排序**后输出,保证同一逻辑内容得到同一字符串。
20
+ * 处理 undefined / 函数 → 省略(与 JSON 语义一致);Date → ISO;Uint8Array → base64 标记。
21
+ * 不追求通用性:只服务本模块的哈希输入(adapter 导出数据 = JSON 安全结构)。
22
+ */
23
+ export function stableStringify(value) {
24
+ const seen = new WeakSet();
25
+ const walk = (v) => {
26
+ if (v === null)
27
+ return 'null';
28
+ if (v === undefined)
29
+ return 'undefined';
30
+ const t = typeof v;
31
+ if (t === 'number')
32
+ return Number.isFinite(v) ? String(v) : 'null';
33
+ if (t === 'boolean')
34
+ return v === true ? 'true' : 'false';
35
+ if (t === 'bigint')
36
+ return `${String(v)}n`;
37
+ if (t === 'string')
38
+ return JSON.stringify(v);
39
+ if (t === 'function' || t === 'symbol')
40
+ return 'undefined';
41
+ // 二进制按内容 sha256 计入,而非 base64 展开:文件类分区的字节就在 data.files[].data 里,
42
+ // 展开成 base64 会让哈希输入膨胀 33% 且对大目录明显变慢;变更检测只需要内容指纹。
43
+ if (v instanceof Uint8Array)
44
+ return `"sha256:${sha256Hex(v)}"`;
45
+ if (v instanceof Date)
46
+ return JSON.stringify(v.toISOString());
47
+ const obj = v;
48
+ if (seen.has(obj))
49
+ return '"<cycle>"';
50
+ seen.add(obj);
51
+ if (Array.isArray(v))
52
+ return `[${v.map(walk).join(',')}]`;
53
+ const entries = Object.entries(obj)
54
+ .filter(([, val]) => val !== undefined && typeof val !== 'function')
55
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
56
+ return `{${entries.map(([k, val]) => `${JSON.stringify(k)}:${walk(val)}`).join(',')}}`;
57
+ };
58
+ return walk(value);
59
+ }
60
+ /**
61
+ * 分区内容指纹。
62
+ * data 走 stableStringify;files(文件类分区)按「相对路径 + 内容 sha256」参与,
63
+ * 使文件重命名/内容变化都能被识别,而不必把字节写进哈希输入。
64
+ */
65
+ export function hashExportSection(section) {
66
+ const files = (section.files ?? [])
67
+ .map((f) => ({ path: f.relativePath, hash: sha256Hex(f.data) }))
68
+ .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
69
+ return sha256Hex(stableStringify({ data: section.data, files }));
70
+ }
71
+ /**
72
+ * 由**已导出的**分区结果构建状态(按 section 升序)。
73
+ *
74
+ * 单一事实源:`captureConfigState` 与「先导出再落快照」的快照路径都必须走这里,
75
+ * 否则两条路径的分区口径(hash / counts / fileCount)会悄悄漂移。
76
+ */
77
+ export function stateFromExports(sections, capturedAt = new Date().toISOString()) {
78
+ const list = [];
79
+ for (const [id, exported] of sections) {
80
+ list.push({
81
+ section: id,
82
+ hash: hashExportSection(exported),
83
+ counts: exported.counts ?? {},
84
+ fileCount: exported.files?.length ?? 0,
85
+ });
86
+ }
87
+ list.sort((a, b) => (a.section < b.section ? -1 : a.section > b.section ? 1 : 0));
88
+ return { capturedAt, sections: list };
89
+ }
90
+ /**
91
+ * 采集当前配置状态。分区按 id 升序输出以保证可比性。
92
+ * 单分区失败 → 跳过(记入 onSectionError),不抛出。
93
+ */
94
+ export async function captureConfigState(adapters, ctx, opts = {}) {
95
+ const only = opts.only !== undefined ? new Set(opts.only) : null;
96
+ const collected = new Map();
97
+ for (const adapter of adapters) {
98
+ if (only !== null && !only.has(adapter.id))
99
+ continue;
100
+ try {
101
+ collected.set(adapter.id, await adapter.export(ctx, { includeSecrets: false, only: [adapter.id] }));
102
+ }
103
+ catch (error) {
104
+ opts.onSectionError?.(adapter.id, error);
105
+ }
106
+ }
107
+ return stateFromExports(collected);
108
+ }
109
+ /** section → SectionState(便于比较与展示);state 为 null/损坏时返回空表(不抛)。 */
110
+ export function stateSections(state) {
111
+ const map = new Map();
112
+ const sections = sectionListOf(state);
113
+ if (sections === null)
114
+ return map;
115
+ for (const s of sections)
116
+ map.set(s.section, s);
117
+ return map;
118
+ }
119
+ /**
120
+ * 取出 state.sections;非数组(含 state 为 null/undefined、或旧格式缺字段)→ null。
121
+ * 用于让 statesEqual 成为**全函数**:绝不因一条损坏的持久化快照而抛 TypeError
122
+ * (undo/redo/status 都直接消费磁盘上的 meta.state)。
123
+ */
124
+ function sectionListOf(state) {
125
+ if (state === null || state === undefined)
126
+ return null;
127
+ const sections = state.sections;
128
+ return Array.isArray(sections) ? sections : null;
129
+ }
130
+ /**
131
+ * 两个状态的内容是否完全一致(分区集合 + 每分区 hash)。
132
+ *
133
+ * 全函数:null / undefined / 缺 sections 的损坏状态一律**不抛**——
134
+ * 任一侧不可解析时退化为引用相等(null vs null 为 true,其余为 false)。
135
+ */
136
+ export function statesEqual(a, b) {
137
+ const left = sectionListOf(a);
138
+ const right = sectionListOf(b);
139
+ if (left === null || right === null)
140
+ return a === b;
141
+ if (left.length !== right.length)
142
+ return false;
143
+ const bySection = new Map();
144
+ for (const s of left)
145
+ bySection.set(s.section, s);
146
+ for (const s of right) {
147
+ const other = bySection.get(s.section);
148
+ if (other === undefined || other.hash !== s.hash)
149
+ return false;
150
+ }
151
+ return true;
152
+ }
153
+ /** 分区级差异(before → after)。顺序确定(按 section 升序)。 */
154
+ export function diffStates(before, after) {
155
+ const b = stateSections(before);
156
+ const a = stateSections(after);
157
+ const changed = [];
158
+ const added = [];
159
+ const removed = [];
160
+ for (const [id, as] of a) {
161
+ const bs = b.get(id);
162
+ if (bs === undefined)
163
+ added.push(id);
164
+ else if (bs.hash !== as.hash)
165
+ changed.push(id);
166
+ }
167
+ for (const id of b.keys()) {
168
+ if (!a.has(id))
169
+ removed.push(id);
170
+ }
171
+ const sortIds = (ids) => ids.sort((x, y) => (x < y ? -1 : x > y ? 1 : 0));
172
+ sortIds(changed);
173
+ sortIds(added);
174
+ sortIds(removed);
175
+ return { changed, added, removed, identical: changed.length === 0 && added.length === 0 && removed.length === 0 };
176
+ }
177
+ //# sourceMappingURL=config-state.js.map