@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,138 @@
1
+ import type { SectionId } from '../schema/types.ts';
2
+ import type { ConfigAdapter, HostContext, ImportPlan, Snapshot, SnapshotStatus, SnapshotStore } from './types.ts';
3
+ /**
4
+ * plugins 分区中「profile 内文件」的 ref 前缀(issue #35):`patchFile:<相对 profile 目录的路径>`。
5
+ * 前缀的存在是为了与 patch **行** id(同样落在 plugins 分区的 ref 空间)区分开——
6
+ * 行 id 是任意字符串,无法靠形状判断它是不是一个路径。
7
+ */
8
+ export declare const PLUGIN_PATCH_REF_PREFIX = "patchFile:";
9
+ /** 解析文件类目标的绝对路径(引擎通用快照与回滚共用) */
10
+ export declare function resolveFileTarget(ctx: HostContext, adapter: SectionId, ref: string): string;
11
+ /**
12
+ * 文件类目标的 home-relative 相对路径(P2-B,Phase 8)。
13
+ * 供导入逐项指纹(analyzer 经 HostContext.fs.readFile(read rel) 读文件算 sha256)
14
+ * 与 journal step.ref 的 posix 规范化。
15
+ *
16
+ * issue #35:`plugins` 分区的两个文件类 ref(`pnpm-workspace.yaml` 与
17
+ * `patchFile:<profile 相对路径>`)不落在 FILE_BASES 的静态基准上,必须显式给 profile ——
18
+ * 否则产出的是**不存在的** home 相对路径(如 `patchFile:patches/a.patch`),
19
+ * 指纹恒为 null,crash 后 reconcile 无法证明该项是否已应用(保守但可避免的精度损失)。
20
+ */
21
+ export declare function resolveFileTargetRel(adapter: SectionId, ref: string, profile?: string): string;
22
+ export interface CreateSnapshotOptions {
23
+ ctx: HostContext;
24
+ plan: ImportPlan;
25
+ sourceZip: string;
26
+ store: SnapshotStore;
27
+ adapters: ConfigAdapter[];
28
+ /** Phase 4:operation-bound binding(journal.operationId / environmentFingerprint / ownerInstanceId / operationType) */
29
+ operationId?: string;
30
+ operationType?: string;
31
+ environmentFingerprint?: string;
32
+ ownerInstanceId?: string;
33
+ }
34
+ /**
35
+ * 生成并落盘快照:只覆盖将被写入的目标。
36
+ * 文件字节经 blobs Map 交给 store.save(SnapshotEntry 契约不含二进制)。
37
+ */
38
+ export declare function createSnapshot(opts: CreateSnapshotOptions): Promise<Snapshot>;
39
+ /**
40
+ * m-retention:保留策略的**结构形状**(故意在 core 内声明,而非 import sync/retention-policy.ts)。
41
+ *
42
+ * 为什么不用 sync 的类型:架构边界测试(tests/architecture-boundaries.test.ts)硬性禁止
43
+ * `core/ → sync/` 反向依赖——core 是与 DSH 解耦的领域层,只允许 node 内置 / core 内部 /
44
+ * schema / utils / security。sync 侧 `RetentionPolicy` 与本接口结构一致,可直接赋值(结构化类型)。
45
+ * 分层选别算法本体在 `src/sync/retention-policy.ts`(selectPruneCandidatesByPolicy),
46
+ * 由宿主(入口层)经 `FileSnapshotStoreOptions.pruneSelector` 注入——core 只定义契约,不反向依赖。
47
+ */
48
+ export interface RetentionPolicyLike {
49
+ /** 最近保留份数 */
50
+ keepLast: number;
51
+ /** 每月保留份数(0 = 关闭) */
52
+ keepMonthly: number;
53
+ /** 每年保留份数(0 = 关闭) */
54
+ keepYearly: number;
55
+ }
56
+ /** core 侧缺省保留策略:与既有 `SNAPSHOT_RETENTION_LIMIT`(最近 10 个)逐字等价。 */
57
+ export declare function defaultRetentionPolicyLike(): RetentionPolicyLike;
58
+ /** 快照保留上限:save 落盘后超过该数量则删除最旧快照目录 */
59
+ export declare const SNAPSHOT_RETENTION_LIMIT = 10;
60
+ /** 纯函数:返回应清理的最旧快照 id(按 createdAt 升序取超限部分;恰好 limit 个 → 空数组)。
61
+ * 参数用最小结构类型,避免引入 restore.ts 的 SnapshotMeta 造成循环 import。 */
62
+ export declare function selectPruneCandidates(metas: ReadonlyArray<{
63
+ id: string;
64
+ createdAt: string;
65
+ }>, limit?: number): string[];
66
+ /**
67
+ * 分层保留选择器契约(core 侧签名;实现由宿主注入,见 sync/retention-policy.ts)。
68
+ * 入参 metas 已剔除 pinned / recovery 引用等豁免项(由 store.prune 完成)。
69
+ */
70
+ export type PruneSelector = (metas: ReadonlyArray<{
71
+ id: string;
72
+ createdAt: string;
73
+ }>, policy: RetentionPolicyLike) => string[];
74
+ /**
75
+ * core 内置的**兜底**选择器:只按 keepLast 取最旧超限部分(= 既有 FIFO 行为)。
76
+ * 分层(keepMonthly/keepYearly)语义需宿主注入真正的 GFS 实现;未注入时分层字段被忽略,
77
+ * 行为等价于改造前(安全侧:宁可少删,绝不因缺实现而多删)。
78
+ */
79
+ export declare const fallbackPruneSelector: PruneSelector;
80
+ export interface FileSnapshotStoreOptions {
81
+ /** 快照根目录(宿主决定,如 ~/.dsh/dsh-config-manager/snapshots) */
82
+ dir: string;
83
+ /**
84
+ * Phase 4 F3:可恢复 / 未收敛 journal 引用的 snapshotId 集合提供者。
85
+ * prune 必须豁免这些 snapshot(绝不可删被 recovery 引用的回滚点)。
86
+ * 缺省 = 空集合(不豁免)。宿主注入 JournalStore.listReferencedSnapshotIds。
87
+ */
88
+ referencedSnapshotIds?: () => Promise<Set<string>>;
89
+ /**
90
+ * Phase 6:自动保留清理的迁移历史回调(best-effort)。
91
+ * prune 删除后回调被清 snapshotId 列表;宿主据其写统一审计史(snapshot-prune kind)。
92
+ * 缺省 = 不记录(不改变 prune 行为、不污染核心引擎)。
93
+ */
94
+ onPrune?: (removedIds: string[]) => void;
95
+ /**
96
+ * m-retention:快照保留策略提供者(GFS 分层;可配置)。
97
+ * 缺省 = `SNAPSHOT_RETENTION_LIMIT`(= 既有的「保留最近 10 个」,行为不变)。
98
+ * 每次 prune 时调用(宿主可从 backup-schedule.json 读取最新策略,用户改完即时生效);
99
+ * 抛错/返回非法值 → 回退缺省策略(绝不因策略读取失败而误删或崩溃)。
100
+ */
101
+ retentionPolicy?: () => RetentionPolicyLike | Promise<RetentionPolicyLike>;
102
+ /**
103
+ * m-retention:分层保留选择器(由宿主注入 sync/retention-policy.ts 的 GFS 实现)。
104
+ * 未注入 → `fallbackPruneSelector`(只按 keepLast,等价既有 FIFO;分层字段被忽略 = 保守少删)。
105
+ */
106
+ pruneSelector?: PruneSelector;
107
+ }
108
+ /** 文件快照存储:<dir>/<id>/snapshot.json + <dir>/<id>/blobs/* */
109
+ export declare class FileSnapshotStore implements SnapshotStore {
110
+ private readonly options;
111
+ constructor(options: FileSnapshotStoreOptions);
112
+ private snapshotDir;
113
+ save(snapshot: Snapshot, blobs?: Map<string, Uint8Array>): Promise<string>;
114
+ /** 保留清理:扫描快照根目录,超限时删除最旧快照目录(损坏/非快照目录跳过;目录缺失容错)。
115
+ * P1-⑧:置顶(pinned=true)的快照豁免自动清理——用户显式保留的导入前回滚点不得被
116
+ * 自动淘汰,只能手动删除(deleteSnapshot)。
117
+ * Phase 4 F3:被 active/quarantine 未收敛 journal 引用的 snapshot(recovery 回滚点)
118
+ * 必须豁免——引用提供者(referencedSnapshotIds)返回的 id 绝不自动清理。 */
119
+ private prune;
120
+ load(id: string): Promise<Snapshot>;
121
+ readBlob(id: string, blobPath: string): Promise<Uint8Array>;
122
+ /** 标记快照生命周期状态:重写 <dir>/<id>/snapshot.json(保留其余字段)。 */
123
+ updateStatus(id: string, status: SnapshotStatus): Promise<void>;
124
+ }
125
+ /**
126
+ * 破坏性内容 hash(稳定,不含 readiness/status):entries + hostFileBackups + beforePlugins。
127
+ * READY 发布与 status 更新不改变此 hash(B-P1-1 修复)。
128
+ */
129
+ export declare function computeMetadataHash(snapshot: Snapshot): string;
130
+ export interface SnapshotVerifyResult {
131
+ ok: boolean;
132
+ reason?: string;
133
+ }
134
+ /**
135
+ * 从磁盘重读并验证快照(F1):id 合法 + snapshot.json 存在 + manifest 存在 + blob hashes 匹配 +
136
+ * metadataHash 匹配 + 必要 blobs 存在 + 路径安全。不信任内存对象。
137
+ */
138
+ export declare function verifySnapshot(snapshotsDir: string, id: string): Promise<SnapshotVerifyResult>;
@@ -0,0 +1,513 @@
1
+ /**
2
+ * Pre-import Snapshot(规范 §16/§27):只保存本次导入将被修改的目标原值。
3
+ * 应用层事务的基础:所有补偿动作基于快照(rollback.ts 逆序执行)。
4
+ *
5
+ * 快照不保存 credential 值(DSH 永不回读值)——credential 条目只记 existed 标志,
6
+ * 回滚时 existed=true 的条目如实标记「值需人工补录」。
7
+ */
8
+ import fs from 'node:fs/promises';
9
+ import path from 'node:path';
10
+ import crypto from 'node:crypto';
11
+ import { parseJsonSafe } from '../utils/json.js';
12
+ import { sha256Hex } from '../utils/hashing.js';
13
+ import { normalizePath } from '../utils/paths.js';
14
+ import { atomicWriteFile } from '../utils/atomic-write.js';
15
+ /** 导入将实际写入的目标 kinds(这些项才需要快照) */
16
+ const EXECUTABLE_KINDS = new Set(['Create', 'Update', 'Install', 'MissingSecret', 'MissingDependency']);
17
+ /**
18
+ * 需要整文件备份的宿主关键文件(相对 $DSH_HOME):
19
+ * - settings.yaml(DSH 配置主存储;不存在时探测 settings.json)
20
+ * - cordis.patch.yml(用户 patch 层)
21
+ * - profiles/<profile>/cordis.patch.yml(profile patch 层,宿主暴露 profile 时)
22
+ */
23
+ const HOST_FILE_CANDIDATES = [
24
+ { relPath: 'settings.yaml' },
25
+ { relPath: 'settings.json' },
26
+ { relPath: 'cordis.patch.yml' },
27
+ ];
28
+ /**
29
+ * 宿主整文件备份(M1):探测存在性,存在的文件字节进 blobs Map 由 store.save 落盘,
30
+ * 全部候选(含 existed:false)登记进 hostFileBackups,供 M2 restore 整文件还原。
31
+ */
32
+ async function backupHostFiles(ctx, blobs) {
33
+ const candidates = [...HOST_FILE_CANDIDATES];
34
+ if (ctx.profile !== undefined && ctx.profile !== '') {
35
+ candidates.push({ relPath: `profiles/${ctx.profile}/cordis.patch.yml` });
36
+ // pnpm-workspace.yaml 决定插件能否安装(allowBuilds/冷静期)→ 一并纳入宿主整文件备份
37
+ candidates.push({ relPath: `profiles/${ctx.profile}/pnpm-workspace.yaml` });
38
+ }
39
+ const backups = [];
40
+ for (const { relPath } of candidates) {
41
+ // settings.yaml 与 settings.json 互斥:主存储存在时不再探测 json 备选
42
+ if (relPath === 'settings.json' && backups.some((b) => b.relPath === 'settings.yaml' && b.existed)) {
43
+ continue;
44
+ }
45
+ let existed = false;
46
+ try {
47
+ existed = await ctx.fs.exists(relPath);
48
+ }
49
+ catch (err) {
50
+ ctx.log.warn(`快照探测宿主文件失败 ${relPath}: ${err instanceof Error ? err.message : String(err)}`);
51
+ existed = false;
52
+ }
53
+ if (!existed) {
54
+ backups.push({ relPath, blobPath: '', existed: false });
55
+ continue;
56
+ }
57
+ const blobPath = `blobs/host/${crypto.randomUUID()}`;
58
+ try {
59
+ blobs.set(blobPath, await ctx.fs.readFile(relPath));
60
+ backups.push({ relPath, blobPath, existed: true });
61
+ }
62
+ catch (err) {
63
+ ctx.log.warn(`快照读取宿主文件失败 ${relPath}: ${err instanceof Error ? err.message : String(err)}`);
64
+ backups.push({ relPath, blobPath: '', existed: false });
65
+ }
66
+ }
67
+ return backups;
68
+ }
69
+ /** 文件类分区的目标基准目录(相对 homeDir;pluginFiles/self 的 ref 已是完整相对路径) */
70
+ const FILE_BASES = {
71
+ skills: 'skills',
72
+ agentPresets: '.agent-presets',
73
+ agentInstructions: '', // homeDir 根(~/.dsh/AGENTS.md)
74
+ pluginFiles: '',
75
+ sessions: 'sessions',
76
+ self: 'dsh-config-manager',
77
+ };
78
+ /**
79
+ * plugins 分区中「profile 内文件」的 ref 前缀(issue #35):`patchFile:<相对 profile 目录的路径>`。
80
+ * 前缀的存在是为了与 patch **行** id(同样落在 plugins 分区的 ref 空间)区分开——
81
+ * 行 id 是任意字符串,无法靠形状判断它是不是一个路径。
82
+ */
83
+ export const PLUGIN_PATCH_REF_PREFIX = 'patchFile:';
84
+ /** profile 目录绝对路径(plugins 分区内文件类目标的基准) */
85
+ function profileDirOf(ctx) {
86
+ const profile = ctx.profile !== undefined && ctx.profile !== '' ? ctx.profile : 'web';
87
+ return path.join(ctx.homeDir, 'profiles', profile);
88
+ }
89
+ /** 解析文件类目标的绝对路径(引擎通用快照与回滚共用) */
90
+ export function resolveFileTarget(ctx, adapter, ref) {
91
+ // plugins 分区的 pnpm-workspace.yaml:位于 profiles/<profile>/ 下(非 FILE_BASES 静态基准)
92
+ if (adapter === 'plugins' && ref === 'pnpm-workspace.yaml') {
93
+ return path.join(profileDirOf(ctx), 'pnpm-workspace.yaml');
94
+ }
95
+ // issue #35:pnpm patch 文件同样位于 profiles/<profile>/ 下,必须能被快照与回滚覆盖,
96
+ // 否则「导入覆盖了目标机原有 patch 文件 → 回滚」会静默丢失原文件。
97
+ if (adapter === 'plugins' && ref.startsWith(PLUGIN_PATCH_REF_PREFIX)) {
98
+ return path.join(profileDirOf(ctx), ref.slice(PLUGIN_PATCH_REF_PREFIX.length));
99
+ }
100
+ const base = FILE_BASES[adapter] ?? '';
101
+ return path.join(ctx.homeDir, base, ref);
102
+ }
103
+ /**
104
+ * 文件类目标的 home-relative 相对路径(P2-B,Phase 8)。
105
+ * 供导入逐项指纹(analyzer 经 HostContext.fs.readFile(read rel) 读文件算 sha256)
106
+ * 与 journal step.ref 的 posix 规范化。
107
+ *
108
+ * issue #35:`plugins` 分区的两个文件类 ref(`pnpm-workspace.yaml` 与
109
+ * `patchFile:<profile 相对路径>`)不落在 FILE_BASES 的静态基准上,必须显式给 profile ——
110
+ * 否则产出的是**不存在的** home 相对路径(如 `patchFile:patches/a.patch`),
111
+ * 指纹恒为 null,crash 后 reconcile 无法证明该项是否已应用(保守但可避免的精度损失)。
112
+ */
113
+ export function resolveFileTargetRel(adapter, ref, profile) {
114
+ if (adapter === 'plugins') {
115
+ const profileDir = `profiles/${profile !== undefined && profile !== '' ? profile : 'web'}`;
116
+ if (ref === 'pnpm-workspace.yaml')
117
+ return normalizePath(`${profileDir}/pnpm-workspace.yaml`);
118
+ if (ref.startsWith(PLUGIN_PATCH_REF_PREFIX)) {
119
+ return normalizePath(`${profileDir}/${ref.slice(PLUGIN_PATCH_REF_PREFIX.length)}`);
120
+ }
121
+ }
122
+ const base = FILE_BASES[adapter] ?? '';
123
+ return normalizePath(path.join(base, ref));
124
+ }
125
+ /** 从计划中收集将被写入的 target(去重) */
126
+ function collectTargets(plan) {
127
+ const seen = new Set();
128
+ const targets = [];
129
+ for (const item of plan.items) {
130
+ if (!EXECUTABLE_KINDS.has(item.kind) || item.target === undefined)
131
+ continue;
132
+ const key = `${item.target.adapter}\u0000${item.target.ref}`;
133
+ if (seen.has(key))
134
+ continue;
135
+ seen.add(key);
136
+ targets.push(item.target);
137
+ }
138
+ return targets;
139
+ }
140
+ /** 引擎通用快照(adapter 未实现 snapshot? 时兜底) */
141
+ async function engineSnapshotEntry(ctx, target) {
142
+ switch (target.adapter) {
143
+ case 'settings':
144
+ case 'ui':
145
+ case 'providers': {
146
+ try {
147
+ const info = await ctx.settings.describe(target.ref);
148
+ return { kind: 'settingsNamespace', adapter: target.adapter, ref: target.ref, before: info.value, revision: info.revision, existed: true };
149
+ }
150
+ catch {
151
+ // 目标不存在该 namespace → 快照标记 existed:false(回滚时无法恢复「不存在」,如实处理)
152
+ return { kind: 'settingsNamespace', adapter: target.adapter, ref: target.ref, before: null, revision: 0, existed: false };
153
+ }
154
+ }
155
+ case 'credentialsStatus': {
156
+ const info = await ctx.credentials.describe(target.ref);
157
+ return { kind: 'credential', adapter: target.adapter, ref: target.ref, before: null, existed: info.configured };
158
+ }
159
+ case 'workspaces': {
160
+ const records = await ctx.workspace.listRecords();
161
+ const rec = records.find((r) => r.id === target.ref);
162
+ return { kind: 'workspaceRecord', adapter: target.adapter, ref: target.ref, before: rec ?? null };
163
+ }
164
+ case 'mcp':
165
+ case 'plugins':
166
+ case 'prompts': {
167
+ // plugins 分区的 pnpm-workspace.yaml / patch 文件 → 整文件快照(file 类,回滚可整文件还原)
168
+ if (target.adapter === 'plugins' && (target.ref === 'pnpm-workspace.yaml' || target.ref.startsWith(PLUGIN_PATCH_REF_PREFIX))) {
169
+ const abs = resolveFileTarget(ctx, target.adapter, target.ref);
170
+ if (!(await ctx.fs.exists(abs))) {
171
+ return { kind: 'file', adapter: target.adapter, ref: target.ref, before: null, existed: false };
172
+ }
173
+ const data = await ctx.fs.readFile(abs);
174
+ return {
175
+ kind: 'file',
176
+ adapter: target.adapter,
177
+ ref: target.ref,
178
+ before: { contentHash: sha256Hex(data) },
179
+ existed: true,
180
+ copiedTo: `blobs/${crypto.randomUUID()}`,
181
+ };
182
+ }
183
+ // patchLine:从组合 patch 文件读取原行(file 为必填的 file 字段约定为 'cordis.patch.yml')
184
+ const file = 'cordis.patch.yml';
185
+ const lines = await ctx.patchFile.readPatchLines(file);
186
+ const line = lines.find((l) => l.lineId === target.ref);
187
+ return { kind: 'patchLine', adapter: target.adapter, ref: target.ref, before: line?.raw ?? null, existed: line !== undefined };
188
+ }
189
+ case 'skills':
190
+ case 'agentPresets':
191
+ case 'agentInstructions':
192
+ case 'pluginFiles':
193
+ case 'sessions':
194
+ case 'self': {
195
+ const abs = resolveFileTarget(ctx, target.adapter, target.ref);
196
+ if (!(await ctx.fs.exists(abs))) {
197
+ return { kind: 'file', adapter: target.adapter, ref: target.ref, before: null, existed: false };
198
+ }
199
+ const data = await ctx.fs.readFile(abs);
200
+ // 文件字节不放进 SnapshotEntry(契约纯净),由 createSnapshot 收集进 blobs Map 统一落盘
201
+ return {
202
+ kind: 'file',
203
+ adapter: target.adapter,
204
+ ref: target.ref,
205
+ before: { contentHash: sha256Hex(data) },
206
+ existed: true,
207
+ copiedTo: `blobs/${crypto.randomUUID()}`,
208
+ };
209
+ }
210
+ default:
211
+ return { kind: 'settingsNamespace', adapter: target.adapter, ref: target.ref, before: null };
212
+ }
213
+ }
214
+ /**
215
+ * 生成并落盘快照:只覆盖将被写入的目标。
216
+ * 文件字节经 blobs Map 交给 store.save(SnapshotEntry 契约不含二进制)。
217
+ */
218
+ export async function createSnapshot(opts) {
219
+ const { ctx, plan, sourceZip, store, adapters } = opts;
220
+ const targets = collectTargets(plan);
221
+ const entries = [];
222
+ const blobs = new Map();
223
+ // 1) 有 adapter.snapshot? 的优先(adapter 更懂自己的数据)
224
+ const byAdapter = new Map();
225
+ for (const t of targets) {
226
+ const list = byAdapter.get(t.adapter) ?? [];
227
+ list.push(t);
228
+ byAdapter.set(t.adapter, list);
229
+ }
230
+ for (const adapter of adapters) {
231
+ const adapterTargets = byAdapter.get(adapter.id);
232
+ if (!adapterTargets || adapterTargets.length === 0)
233
+ continue;
234
+ if (adapter.snapshot) {
235
+ const adapterEntries = await adapter.snapshot(adapterTargets, ctx);
236
+ entries.push(...adapterEntries);
237
+ byAdapter.delete(adapter.id);
238
+ }
239
+ }
240
+ // 2) 引擎通用快照兜底
241
+ for (const [adapterId, remaining] of byAdapter) {
242
+ for (const target of remaining) {
243
+ const entry = await engineSnapshotEntry(ctx, target);
244
+ // 文件字节:文件类条目在引擎快照里读出内容 → blobs
245
+ if (entry.kind === 'file' && entry.copiedTo && entry.existed) {
246
+ const abs = resolveFileTarget(ctx, adapterId, target.ref);
247
+ blobs.set(entry.copiedTo, await ctx.fs.readFile(abs));
248
+ }
249
+ entries.push(entry);
250
+ }
251
+ }
252
+ // 3) 宿主整文件备份(M1):settings.yaml/settings.json + 用户/ profile 层 cordis.patch.yml
253
+ const hostFileBackups = await backupHostFiles(ctx, blobs);
254
+ // 4) 导入前插件清单(M2 restore 撤销插件对比基准;读取失败不阻断快照)
255
+ let beforePlugins = [];
256
+ try {
257
+ beforePlugins = await ctx.plugins.listInstalled();
258
+ }
259
+ catch (err) {
260
+ ctx.log.warn(`快照登记插件清单失败: ${err instanceof Error ? err.message : String(err)}`);
261
+ }
262
+ const snapshot = {
263
+ id: crypto.randomUUID(),
264
+ createdAt: new Date().toISOString(),
265
+ sourceZip,
266
+ entries,
267
+ status: 'pending',
268
+ beforePlugins,
269
+ hostFileBackups,
270
+ // Phase 4:operation-bound binding(journal ↔ snapshot 双向一致)
271
+ ...(opts.operationId !== undefined ? { operationId: opts.operationId } : {}),
272
+ ...(opts.operationType !== undefined ? { operationType: opts.operationType } : {}),
273
+ ...(opts.environmentFingerprint !== undefined ? { environmentFingerprint: opts.environmentFingerprint } : {}),
274
+ ...(opts.ownerInstanceId !== undefined ? { ownerInstanceId: opts.ownerInstanceId } : {}),
275
+ };
276
+ // file 条目登记 snapshotId,回滚读 blob 时定位快照目录
277
+ for (const entry of snapshot.entries) {
278
+ if (entry.kind === 'file')
279
+ entry.snapshotId = snapshot.id;
280
+ }
281
+ await store.save(snapshot, blobs);
282
+ return snapshot;
283
+ }
284
+ /** core 侧缺省保留策略:与既有 `SNAPSHOT_RETENTION_LIMIT`(最近 10 个)逐字等价。 */
285
+ export function defaultRetentionPolicyLike() {
286
+ return { keepLast: SNAPSHOT_RETENTION_LIMIT, keepMonthly: 0, keepYearly: 0 };
287
+ }
288
+ /** 快照保留上限:save 落盘后超过该数量则删除最旧快照目录 */
289
+ export const SNAPSHOT_RETENTION_LIMIT = 10;
290
+ /** 纯函数:返回应清理的最旧快照 id(按 createdAt 升序取超限部分;恰好 limit 个 → 空数组)。
291
+ * 参数用最小结构类型,避免引入 restore.ts 的 SnapshotMeta 造成循环 import。 */
292
+ export function selectPruneCandidates(metas, limit = SNAPSHOT_RETENTION_LIMIT) {
293
+ if (metas.length <= limit)
294
+ return [];
295
+ const sorted = [...metas].sort((a, b) => (a.createdAt < b.createdAt ? -1 : a.createdAt > b.createdAt ? 1 : 0));
296
+ return sorted.slice(0, sorted.length - limit).map((m) => m.id);
297
+ }
298
+ /**
299
+ * core 内置的**兜底**选择器:只按 keepLast 取最旧超限部分(= 既有 FIFO 行为)。
300
+ * 分层(keepMonthly/keepYearly)语义需宿主注入真正的 GFS 实现;未注入时分层字段被忽略,
301
+ * 行为等价于改造前(安全侧:宁可少删,绝不因缺实现而多删)。
302
+ */
303
+ export const fallbackPruneSelector = (metas, policy) => selectPruneCandidates(metas, policy.keepLast);
304
+ /** 文件快照存储:<dir>/<id>/snapshot.json + <dir>/<id>/blobs/* */
305
+ export class FileSnapshotStore {
306
+ options;
307
+ constructor(options) {
308
+ this.options = options;
309
+ }
310
+ snapshotDir(id) {
311
+ return path.join(this.options.dir, id);
312
+ }
313
+ async save(snapshot, blobs = new Map()) {
314
+ const dir = this.snapshotDir(snapshot.id);
315
+ await fs.mkdir(path.join(dir, 'blobs'), { recursive: true });
316
+ // 1) 写 blobs
317
+ for (const [blobPath, data] of blobs) {
318
+ const target = path.join(dir, blobPath);
319
+ if (!target.startsWith(dir))
320
+ throw new Error(`快照 blob 路径越界: ${blobPath}`);
321
+ await fs.mkdir(path.dirname(target), { recursive: true });
322
+ await atomicWriteFile(target, data);
323
+ }
324
+ // 2) 写 snapshot.json(readiness='CREATING',未完成不可用)
325
+ const creating = { ...snapshot, readiness: 'CREATING' };
326
+ await atomicWriteFile(path.join(dir, 'snapshot.json'), JSON.stringify(creating, null, 2));
327
+ // 3) 写 manifest(blob hashes + 破坏性内容 hash;metadataHash 稳定,不含 readiness/status)
328
+ const blobHashes = {};
329
+ for (const [blobPath, data] of blobs) {
330
+ blobHashes[blobPath] = sha256Hex(data);
331
+ }
332
+ const manifest = {
333
+ schemaVersion: 1,
334
+ snapshotId: snapshot.id,
335
+ entryCount: snapshot.entries.length,
336
+ blobHashes,
337
+ metadataHash: computeMetadataHash(snapshot),
338
+ };
339
+ await atomicWriteFile(path.join(dir, 'manifest.json'), JSON.stringify(manifest, null, 2));
340
+ // 4) 验证(从磁盘重读,不信任内存对象)
341
+ const verified = await verifySnapshot(this.options.dir, snapshot.id);
342
+ if (!verified.ok) {
343
+ throw new Error(`快照 ${snapshot.id} 验证失败: ${verified.reason}`);
344
+ }
345
+ // 5) 原子发布 READY(重写 snapshot.json readiness='READY';metadataHash 稳定不受影响)
346
+ const ready = { ...snapshot, readiness: 'READY' };
347
+ await atomicWriteFile(path.join(dir, 'snapshot.json'), JSON.stringify(ready, null, 2));
348
+ // 6) 保留清理(F13:prune 失败不中止已 durable 的 READY 快照——日志记录,upgrade 可继续)
349
+ try {
350
+ await this.prune();
351
+ }
352
+ catch (err) {
353
+ // 快照已 READY + verified;prune 失败(EBUSY/EPERM)不应把刚成功的快照判为 unusable
354
+ }
355
+ return snapshot.id;
356
+ }
357
+ /** 保留清理:扫描快照根目录,超限时删除最旧快照目录(损坏/非快照目录跳过;目录缺失容错)。
358
+ * P1-⑧:置顶(pinned=true)的快照豁免自动清理——用户显式保留的导入前回滚点不得被
359
+ * 自动淘汰,只能手动删除(deleteSnapshot)。
360
+ * Phase 4 F3:被 active/quarantine 未收敛 journal 引用的 snapshot(recovery 回滚点)
361
+ * 必须豁免——引用提供者(referencedSnapshotIds)返回的 id 绝不自动清理。 */
362
+ async prune() {
363
+ const dir = this.options.dir;
364
+ const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => []);
365
+ const referenced = await this.options.referencedSnapshotIds?.().catch(() => new Set()) ?? new Set();
366
+ // m-retention:策略读取失败 → 回退缺省(绝不因策略提供者异常而误删/崩溃)
367
+ const policy = await Promise.resolve()
368
+ .then(() => this.options.retentionPolicy?.() ?? defaultRetentionPolicyLike())
369
+ .catch(() => defaultRetentionPolicyLike());
370
+ const selector = this.options.pruneSelector ?? fallbackPruneSelector;
371
+ const metas = [];
372
+ for (const entry of entries) {
373
+ if (!entry.isDirectory())
374
+ continue;
375
+ try {
376
+ const parsed = parseJsonSafe(await fs.readFile(path.join(dir, entry.name, 'snapshot.json'), 'utf8'));
377
+ if (typeof parsed.id !== 'string' || parsed.id === '' || typeof parsed.createdAt !== 'string')
378
+ continue;
379
+ if (parsed.pinned === true)
380
+ continue; // 置顶快照豁免自动清理
381
+ if (referenced.has(parsed.id))
382
+ continue; // Phase 4 F3:recovery 引用豁免
383
+ metas.push({ id: parsed.id, createdAt: parsed.createdAt });
384
+ }
385
+ catch {
386
+ // 损坏 / 非快照目录:跳过(与 listSnapshots 语义一致)
387
+ }
388
+ }
389
+ const removedIds = [];
390
+ for (const id of selector(metas, policy)) {
391
+ const target = path.join(dir, id);
392
+ if (!target.startsWith(dir))
393
+ continue; // 越界 id 跳过(不删、不抛,同 save/readBlob 包含性约定)
394
+ await fs.rm(target, { recursive: true, force: true });
395
+ removedIds.push(id);
396
+ }
397
+ // Phase 6:自动保留清理历史回调(best-effort;不改变 prune 行为)
398
+ if (removedIds.length > 0) {
399
+ try {
400
+ this.options.onPrune?.(removedIds);
401
+ }
402
+ catch { /* 历史回调失败不阻断 */ }
403
+ }
404
+ }
405
+ async load(id) {
406
+ const raw = await fs.readFile(path.join(this.snapshotDir(id), 'snapshot.json'), 'utf8');
407
+ return parseJsonSafe(raw);
408
+ }
409
+ async readBlob(id, blobPath) {
410
+ const target = path.join(this.snapshotDir(id), blobPath);
411
+ const dir = this.options.dir;
412
+ if (!target.startsWith(dir))
413
+ throw new Error(`快照 blob 路径越界: ${blobPath}`);
414
+ return fs.readFile(target);
415
+ }
416
+ /** 标记快照生命周期状态:重写 <dir>/<id>/snapshot.json(保留其余字段)。 */
417
+ async updateStatus(id, status) {
418
+ const file = path.join(this.snapshotDir(id), 'snapshot.json');
419
+ const snapshot = parseJsonSafe(await fs.readFile(file, 'utf8'));
420
+ snapshot.status = status;
421
+ await atomicWriteFile(file, JSON.stringify(snapshot, null, 2));
422
+ }
423
+ }
424
+ /* ---------------- Phase 4:manifest / verifySnapshot(F1) ---------------- */
425
+ /**
426
+ * 破坏性内容 hash(稳定,不含 readiness/status):entries + hostFileBackups + beforePlugins。
427
+ * READY 发布与 status 更新不改变此 hash(B-P1-1 修复)。
428
+ */
429
+ export function computeMetadataHash(snapshot) {
430
+ const destructive = {
431
+ entries: snapshot.entries,
432
+ hostFileBackups: snapshot.hostFileBackups ?? [],
433
+ beforePlugins: snapshot.beforePlugins ?? [],
434
+ };
435
+ return sha256Hex(new TextEncoder().encode(JSON.stringify(destructive)));
436
+ }
437
+ /**
438
+ * 从磁盘重读并验证快照(F1):id 合法 + snapshot.json 存在 + manifest 存在 + blob hashes 匹配 +
439
+ * metadataHash 匹配 + 必要 blobs 存在 + 路径安全。不信任内存对象。
440
+ */
441
+ export async function verifySnapshot(snapshotsDir, id) {
442
+ const dir = path.join(snapshotsDir, id);
443
+ if (!dir.startsWith(path.resolve(snapshotsDir) + path.sep)) {
444
+ return { ok: false, reason: '快照路径越界' };
445
+ }
446
+ let raw;
447
+ try {
448
+ raw = await fs.readFile(path.join(dir, 'snapshot.json'), 'utf8');
449
+ }
450
+ catch {
451
+ return { ok: false, reason: 'snapshot.json 缺失' };
452
+ }
453
+ const snapshot = parseJsonSafe(raw);
454
+ if (snapshot === null || typeof snapshot !== 'object' || typeof snapshot.id !== 'string' || snapshot.id === '') {
455
+ return { ok: false, reason: 'snapshot.json 非法' };
456
+ }
457
+ if (snapshot.id !== id) {
458
+ return { ok: false, reason: 'snapshot.id 与目录不匹配' };
459
+ }
460
+ let manifestRaw;
461
+ try {
462
+ manifestRaw = await fs.readFile(path.join(dir, 'manifest.json'), 'utf8');
463
+ }
464
+ catch {
465
+ return { ok: false, reason: 'manifest.json 缺失' };
466
+ }
467
+ const manifest = parseJsonSafe(manifestRaw);
468
+ if (manifest === null || typeof manifest !== 'object' || manifest.snapshotId !== id) {
469
+ return { ok: false, reason: 'manifest 非法或 snapshotId 不匹配' };
470
+ }
471
+ // entryCount 匹配(manifest 声明条目数与实际一致,防计数篡改)
472
+ if (typeof manifest.entryCount !== 'number' || manifest.entryCount !== snapshot.entries.length) {
473
+ return { ok: false, reason: 'manifest.entryCount 与 snapshot.entries 不一致(篡改)' };
474
+ }
475
+ // metadataHash 匹配(破坏性内容)
476
+ if (computeMetadataHash(snapshot) !== manifest.metadataHash) {
477
+ return { ok: false, reason: 'metadataHash 不匹配(破坏性内容被篡改)' };
478
+ }
479
+ // blob hashes 匹配 + 存在
480
+ for (const [blobPath, expectedHash] of Object.entries(manifest.blobHashes)) {
481
+ const target = path.join(dir, blobPath);
482
+ if (!target.startsWith(dir))
483
+ return { ok: false, reason: `blob 路径越界: ${blobPath}` };
484
+ let data;
485
+ try {
486
+ data = await fs.readFile(target);
487
+ }
488
+ catch {
489
+ return { ok: false, reason: `blob 缺失: ${blobPath}` };
490
+ }
491
+ if (sha256Hex(data) !== expectedHash) {
492
+ return { ok: false, reason: `blob hash 不匹配: ${blobPath}` };
493
+ }
494
+ }
495
+ // Review C P2:snapshot.json 引用的每个 blob(entries.copiedTo / hostFileBackups.blobPath)都必须被 manifest 覆盖,
496
+ // 否则攻击者可新增未被哈希校验的 blob(restore 会读+写)→ 覆盖任意 home 文件。
497
+ const referencedBlobs = [];
498
+ for (const e of snapshot.entries) {
499
+ if (e.kind === 'file' && e.copiedTo && e.copiedTo !== '')
500
+ referencedBlobs.push(e.copiedTo);
501
+ }
502
+ for (const hb of snapshot.hostFileBackups ?? []) {
503
+ if (hb.blobPath && hb.blobPath !== '' && hb.existed)
504
+ referencedBlobs.push(hb.blobPath);
505
+ }
506
+ for (const relBlob of referencedBlobs) {
507
+ if (!(relBlob in manifest.blobHashes)) {
508
+ return { ok: false, reason: `snapshot 引用的 blob 未在 manifest 中: ${relBlob}` };
509
+ }
510
+ }
511
+ return { ok: true };
512
+ }
513
+ //# sourceMappingURL=backup.js.map