@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,518 @@
1
+ /**
2
+ * Durable Operation Journal(Phase 3:Crash Journal)。
3
+ *
4
+ * 职责(严格限定):journal 的 schema / 状态机 / 持久化 / atomic 更新 / active 扫描 /
5
+ * terminal 判定 / move(active→completed)/ quarantine / recovery-history / retention /
6
+ * safe-mode 标记 / environmentFingerprint 存储辅助。
7
+ *
8
+ * **JournalStore 不决定** rollback / resume / transaction outcome —— 那些属于
9
+ * `MutationTransactionCoordinator`(transaction-coordinator.ts)与 `Reconciler`(reconcile.ts)。
10
+ *
11
+ * 安全不变量(Phase 1 + Phase 3 Rev 3):
12
+ * - journal 更新一律经 Phase 1 `atomicWriteFile`(单文件 old-or-new,不 truncate-write)。
13
+ * - journal 文件 mode 0600;transactions 目录 0700;symlink reject 写 + lstat 读。
14
+ * - 只把 `<uuid>.json` 当作 journal;忽略 `.dshcm.*.tmp` 及其它非 journal 文件。
15
+ * - 不保存任何 secret(错误/recovery.reason 须过强 redaction)。
16
+ */
17
+ import fs from 'node:fs/promises';
18
+ import path from 'node:path';
19
+ import os from 'node:os';
20
+ import crypto from 'node:crypto';
21
+ import { atomicWriteFile } from '../utils/atomic-write.ts';
22
+ import { redact } from '../security/redaction.ts';
23
+
24
+ /** 高熵值形状(长 hex/base64/随机 id):redact() 覆盖不了任意 secret,journal 级强脱敏补挡。 */
25
+ const HIGH_ENTROPY_RE = /([A-Za-z0-9+/_=-]{28,})/g;
26
+
27
+ /** 结构化时间戳/文件名形态:日期段必须由连字符连接(ISO 日期或紧凑时间戳)。
28
+ * 连字符不存在于 hex/base64 token 中,豁免不会误放行随机密钥。 */
29
+ const DATE_STAMP_RE = /\d{4}-\d{2}-\d{2}|\d{8}-\d{6}/;
30
+
31
+ /** 高熵长 token 掩码(日期戳形态豁免;回调逐个 run 判定,避免误伤文件名/时间戳)。 */
32
+ function maskHighEntropy(text: string): string {
33
+ return text.replace(HIGH_ENTROPY_RE, (run) => (DATE_STAMP_RE.test(run) ? run : '[REDACTED]'));
34
+ }
35
+
36
+ /**
37
+ * journal 专用文本脱敏(Security P1-1 / §29 已并入):
38
+ * 现有 redact()(结构字段 + 已知值形状)+ 高熵长 token 掩码。
39
+ * 用于 error / recovery.reason 等可能嵌入任意值的字段。
40
+ */
41
+ export function redactJournalText(text: string): string {
42
+ let out = text;
43
+ try { out = redact(out); } catch { /* 脱敏失败保守处理 */ }
44
+ return maskHighEntropy(out);
45
+ }
46
+
47
+ // ---------- 常量 ----------
48
+
49
+ export const JOURNAL_SCHEMA_VERSION = 1;
50
+ export const TRANSACTIONS_DIR = 'transactions';
51
+ export const ACTIVE_DIR = 'active';
52
+ export const COMPLETED_DIR = 'completed';
53
+ export const QUARANTINE_DIR = 'quarantine';
54
+ export const RECOVERY_HISTORY_DIR = 'recovery-history';
55
+ export const SAFE_MODE_MARKER = 'safe-mode';
56
+ export const NEEDS_ATTENTION_SIDECAR_SUFFIX = '.needs-attention';
57
+ /** 只把 `<uuid>.json` 视作 journal(uuid v4 / v4-like),忽略 tmp 等。 */
58
+ const UUID_BASENAME_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.json$/i;
59
+ const TMP_PREFIX = '.dshcm.';
60
+
61
+ export const VALID_OPERATION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
62
+
63
+ // ---------- Journal 类型 ----------
64
+
65
+ export type JournalState =
66
+ | 'CREATED' | 'SNAPSHOT_CREATED' | 'APPLYING' | 'VALIDATING' | 'COMMITTED'
67
+ | 'ROLLING_BACK' | 'ROLLED_BACK'
68
+ | 'RECOVERING' | 'RECOVERED'
69
+ | 'NEEDS_ATTENTION';
70
+
71
+ /** terminal states:进入后 operation 不可再改向(只可 move/quarantine/retention) */
72
+ export const TERMINAL_STATES: ReadonlySet<JournalState> = new Set<JournalState>([
73
+ 'COMMITTED', 'ROLLED_BACK', 'RECOVERED', 'NEEDS_ATTENTION',
74
+ ]);
75
+
76
+ /** 合法状态迁移表(严格;禁止无效 transition)。纯函数,供单测。 */
77
+ export const ALLOWED_TRANSITIONS: Record<JournalState, JournalState[]> = {
78
+ CREATED: ['SNAPSHOT_CREATED', 'APPLYING', 'NEEDS_ATTENTION', 'RECOVERED', 'ROLLED_BACK'],
79
+ SNAPSHOT_CREATED: ['APPLYING', 'NEEDS_ATTENTION', 'RECOVERED', 'ROLLED_BACK'],
80
+ APPLYING: ['VALIDATING', 'ROLLING_BACK', 'NEEDS_ATTENTION', 'RECOVERED'],
81
+ VALIDATING: ['COMMITTED', 'ROLLING_BACK', 'NEEDS_ATTENTION', 'RECOVERED'],
82
+ COMMITTED: ['RECOVERED'], // COMMITTED 后只能由 recovery 规整为 RECOVERED(补记)
83
+ ROLLING_BACK: ['ROLLED_BACK', 'NEEDS_ATTENTION'],
84
+ ROLLED_BACK: ['RECOVERED'], // ROLLED_BACK 后 recovery 可规整为 RECOVERED(幂等)
85
+ RECOVERING: ['RECOVERED', 'ROLLED_BACK', 'NEEDS_ATTENTION'],
86
+ RECOVERED: [],
87
+ NEEDS_ATTENTION: ['RECOVERING', 'ROLLED_BACK', 'RECOVERED'], // 用户确认后可进入 recovery 流程
88
+ };
89
+
90
+ export function isValidTransition(from: JournalState, to: JournalState): boolean {
91
+ return (ALLOWED_TRANSITIONS[from] ?? []).includes(to);
92
+ }
93
+
94
+ export function isTerminalState(s: JournalState): boolean {
95
+ return TERMINAL_STATES.has(s);
96
+ }
97
+
98
+ export type StepStatus = 'planned' | 'done' | 'failed' | 'skipped' | 'attention';
99
+
100
+ export interface JournalStep {
101
+ adapter: string;
102
+ ref: string;
103
+ kind: string;
104
+ /** 外部副作用(插件/Git/WebDAV/reinstall)——crash 后不可证明,一律保守 */
105
+ external: boolean;
106
+ /** side effect 前目标内容指纹(null = 不可指纹) */
107
+ beforeFp: string | null;
108
+ /** side effect 完成后重读磁盘算出的指纹(null = 不可指纹) */
109
+ afterFp: string | null;
110
+ status: StepStatus;
111
+ appliedAt: string | null;
112
+ /** 该项的结论文案(issue #35:安装失败曾被记成 `skipped` 且无 message,
113
+ * 事后审计只能看到「用户跳过了这些插件」的错误结论)。可选,历史 journal 无此字段。 */
114
+ message?: string | null;
115
+ }
116
+
117
+ export interface JournalCommit { at: string | null; validated: boolean; validationWarnings: string[]; }
118
+ export interface JournalRollback {
119
+ attemptedAt: string | null;
120
+ full: boolean;
121
+ failed: string[];
122
+ /** 回滚 WAL:已补偿的 entry 序号(crash during rollback 判定用) */
123
+ entryDone: Record<number, boolean>;
124
+ }
125
+ /** recovery 元数据;outcome 为 true 表示已达 terminal */
126
+ export interface JournalRecovery {
127
+ attemptedAt: string | null;
128
+ outcome: 'RECOVERED' | 'ROLLED_BACK' | 'NEEDS_ATTENTION' | null;
129
+ reason: string;
130
+ attempts: number;
131
+ }
132
+
133
+ /** Phase 5 post-recovery verification verdict(§6.3)。 */
134
+ export type RecoveryVerificationVerdict =
135
+ | 'MATCH' // 所有关键状态与 trusted snapshot 匹配 → terminal(可 COMMITTED)
136
+ | 'PARTIAL_MATCH' // 关键状态匹配,但存在不可验证项(如凭据值不可回读)→ terminal + 警告
137
+ | 'MISMATCH' // 关键状态不匹配(恢复未生效 / 部分残留)→ NEEDS_ATTENTION(不 COMMITTED)
138
+ | 'VERIFICATION_ERROR'; // 验证本身失败(无法读文件 / 快照损坏)→ NEEDS_ATTENTION(不 COMMITTED)
139
+
140
+ /** Phase 5 post-recovery verification 结果(journal 可选字段,additive,不 bump schema)。 */
141
+ export interface RecoveryVerification {
142
+ verdict: RecoveryVerificationVerdict;
143
+ /** 每项检查结果(写入前过 redactJournalText 强脱敏)。 */
144
+ details: string[];
145
+ /** 需人工处理项(凭据等;写入前过 redactJournalText 强脱敏)。 */
146
+ manualHints: string[];
147
+ at: string;
148
+ }
149
+
150
+ /** 单个 operation 的 durable journal。不保存任何 secret 值。 */
151
+ export interface OperationJournal {
152
+ schemaVersion: number;
153
+ operationId: string;
154
+ operationType: string;
155
+ createdAt: string;
156
+ updatedAt: string;
157
+ state: JournalState;
158
+ /** Journal→Lock 单向绑定(不回填 environment.lock) */
159
+ ownerInstanceId: string;
160
+ lockId: string;
161
+ packageVersion: string;
162
+ environmentFingerprint: string;
163
+ snapshotId: string | null;
164
+ plannedSteps: string[];
165
+ steps: Record<string, JournalStep>;
166
+ commit: JournalCommit;
167
+ rollback: JournalRollback;
168
+ recovery: JournalRecovery;
169
+ /** 最后错误/原因文本(已 redact;非 secret) */
170
+ error: string;
171
+ /**
172
+ * Phase 5:post-recovery verification 结果(可选,additive)。
173
+ * 旧 journal 无此字段(parseSafe 不受影响,schemaVersion 仍为 1)。
174
+ * details/manualHints 写入前必须过 redactJournalText(journal 安全不变量:不保存 secret)。
175
+ */
176
+ recoveryVerification?: RecoveryVerification;
177
+ }
178
+
179
+ export function createJournalEntry(
180
+ operationType: string,
181
+ lockCtx: { operationId: string; ownerInstanceId: string; lockId: string; packageVersion: string; environmentFingerprint: string },
182
+ now: string,
183
+ ): OperationJournal {
184
+ return {
185
+ schemaVersion: JOURNAL_SCHEMA_VERSION,
186
+ operationId: lockCtx.operationId,
187
+ operationType,
188
+ createdAt: now,
189
+ updatedAt: now,
190
+ state: 'CREATED',
191
+ ownerInstanceId: lockCtx.ownerInstanceId,
192
+ lockId: lockCtx.lockId,
193
+ packageVersion: lockCtx.packageVersion,
194
+ environmentFingerprint: lockCtx.environmentFingerprint,
195
+ snapshotId: null,
196
+ plannedSteps: [],
197
+ steps: {},
198
+ commit: { at: null, validated: false, validationWarnings: [] },
199
+ rollback: { attemptedAt: null, full: false, failed: [], entryDone: {} },
200
+ recovery: { attemptedAt: null, outcome: null, reason: '', attempts: 0 },
201
+ error: '',
202
+ };
203
+ }
204
+
205
+ /** 把 journal 从旧状态迁移到新状态(校验合法 transition;非法抛错)。纯函数。 */
206
+ export function transitionJournalState(j: OperationJournal, to: JournalState): OperationJournal {
207
+ if (!isValidTransition(j.state, to)) {
208
+ throw new Error(`非法 journal state transition: ${j.state} → ${to} (op ${j.operationId})`);
209
+ }
210
+ return { ...j, state: to, updatedAt: new Date().toISOString() };
211
+ }
212
+
213
+ // ---------- IO / 存储 ----------
214
+
215
+ /** 可注入 IO(测 failure injection;默认包 node:fs/promises)。 */
216
+ export interface JournalIo {
217
+ mkdir(dir: string, opts: { recursive: boolean }): Promise<void>;
218
+ readFileText(p: string): Promise<string>;
219
+ writeAll(target: string, content: string): Promise<void>;
220
+ rename(a: string, b: string): Promise<void>;
221
+ readdirNames(dir: string): Promise<string[]>;
222
+ readdirEntries(dir: string): Promise<Array<{ name: string; isDirectory(): boolean }>>;
223
+ lstat(p: string): Promise<{ isSymbolicLink(): boolean } | null>;
224
+ rm(p: string, opts: { recursive?: boolean; force?: boolean }): Promise<void>;
225
+ exists(p: string): Promise<boolean>;
226
+ }
227
+
228
+ const defaultIo: JournalIo = {
229
+ async mkdir(d, o) { await fs.mkdir(d, o); },
230
+ async readFileText(p) { return (await fs.readFile(p, 'utf8')).toString(); },
231
+ async writeAll(t, c) { await atomicWriteFile(t, c, { mode: 0o600, symlink: 'reject' }); },
232
+ async rename(a, b) { await fs.rename(a, b); },
233
+ async readdirNames(d) { try { return await fs.readdir(d); } catch { return []; } },
234
+ async readdirEntries(d) { try { return await fs.readdir(d, { withFileTypes: true }); } catch { return []; } },
235
+ async lstat(p) { try { return await fs.lstat(p); } catch { return null; } },
236
+ async rm(p, o) { await fs.rm(p, o); },
237
+ async exists(p) { try { await fs.access(p); return true; } catch { return false; } },
238
+ };
239
+
240
+ export interface JournalStoreOptions {
241
+ transactionsDir: string;
242
+ io?: JournalIo;
243
+ }
244
+
245
+ /** journal 严格 UUID 校验(防文件名穿越)。 */
246
+ export function isValidOperationId(id: unknown): id is string {
247
+ return typeof id === 'string' && VALID_OPERATION_ID_RE.test(id);
248
+ }
249
+
250
+ /** 判断文件 basename 是否是可追踪的 journal(<uuid>.json)。忽略 tmp 及其它。 */
251
+ export function isJournalBasename(name: string): boolean {
252
+ if (!UUID_BASENAME_RE.test(name)) return false;
253
+ return !name.startsWith(TMP_PREFIX);
254
+ }
255
+
256
+ function parseSafe(text: string): OperationJournal | null {
257
+ try {
258
+ const parsed = JSON.parse(text) as OperationJournal;
259
+ if (parsed === null || typeof parsed !== 'object') return null;
260
+ if (parsed.schemaVersion !== JOURNAL_SCHEMA_VERSION) return null;
261
+ if (isValidOperationId(parsed.operationId) !== true) return null;
262
+ if (typeof parsed.state !== 'string' || !isTerminalState(parsed.state) && !Object.keys(ALLOWED_TRANSITIONS).includes(parsed.state)) return null;
263
+ return parsed;
264
+ } catch {
265
+ return null;
266
+ }
267
+ }
268
+
269
+ /** JournalStore:journal 持久化原语。不决定 transaction outcome。 */
270
+ export class JournalStore {
271
+ private readonly transactionsDir: string;
272
+ private readonly io: JournalIo;
273
+
274
+ constructor(opts: JournalStoreOptions) {
275
+ this.transactionsDir = opts.transactionsDir;
276
+ this.io = opts.io ?? defaultIo;
277
+ }
278
+
279
+ private activeDir(): string { return path.join(this.transactionsDir, ACTIVE_DIR); }
280
+ private completedDir(): string { return path.join(this.transactionsDir, COMPLETED_DIR); }
281
+ private quarantineDir(): string { return path.join(this.transactionsDir, QUARANTINE_DIR); }
282
+ private recoveryHistoryDir(): string { return path.join(this.transactionsDir, RECOVERY_HISTORY_DIR); }
283
+ private safeModePath(): string { return path.join(this.transactionsDir, SAFE_MODE_MARKER); }
284
+ private activePath(id: string): string { return path.join(this.activeDir(), `${id}.json`); }
285
+ private completedPath(id: string): string { return path.join(this.completedDir(), `${id}.json`); }
286
+ private quarantinePath(id: string): string { return path.join(this.quarantineDir(), `${id}.json`); }
287
+
288
+ async ensureDirs(): Promise<void> {
289
+ await this.io.mkdir(this.transactionsDir, { recursive: true });
290
+ await this.io.mkdir(this.activeDir(), { recursive: true });
291
+ await this.io.mkdir(this.completedDir(), { recursive: true });
292
+ await this.io.mkdir(this.quarantineDir(), { recursive: true });
293
+ await this.io.mkdir(this.recoveryHistoryDir(), { recursive: true });
294
+ }
295
+
296
+ /** 写 journal(atomic + 0600 + symlink reject)。返回写入后的 journal。 */
297
+ async persist(operationId: string, j: OperationJournal): Promise<OperationJournal> {
298
+ if (!isValidOperationId(operationId)) throw new Error(`非法 operationId: ${JSON.stringify(operationId)}`);
299
+ await this.ensureDirs();
300
+ const updated = { ...j, updatedAt: new Date().toISOString() };
301
+ await this.io.writeAll(this.activePath(operationId), `${JSON.stringify(updated, null, 2)}\n`);
302
+ return updated;
303
+ }
304
+
305
+ /** 创建新 journal(CREATED)。调用方保证 active≤1。 */
306
+ async create(entry: OperationJournal): Promise<OperationJournal> {
307
+ return this.persist(entry.operationId, entry);
308
+ }
309
+
310
+ async load(operationId: string): Promise<OperationJournal | null> {
311
+ if (!isValidOperationId(operationId)) return null;
312
+ const p = this.activePath(operationId);
313
+ if (await this.io.exists(p)) {
314
+ const text = await this.io.readFileText(p);
315
+ return parseSafe(text);
316
+ }
317
+ const c = this.completedPath(operationId);
318
+ if (await this.io.exists(c)) {
319
+ const text = await this.io.readFileText(c);
320
+ return parseSafe(text);
321
+ }
322
+ return null;
323
+ }
324
+
325
+ async loadActive(operationId: string): Promise<OperationJournal | null> {
326
+ if (!isValidOperationId(operationId)) return null;
327
+ const p = this.activePath(operationId);
328
+ if (!(await this.io.exists(p))) return null;
329
+ if ((await this.io.lstat(p))?.isSymbolicLink() === true) return null; // symlink 防御
330
+ const text = await this.io.readFileText(p);
331
+ return parseSafe(text);
332
+ }
333
+
334
+ /** 更新 journal(按 updater 修改后原子持久化)。不负责 transition 校验(调用方经 transitionJournalState)。 */
335
+ async update(operationId: string, updater: (j: OperationJournal) => OperationJournal): Promise<OperationJournal> {
336
+ const cur = await this.loadActive(operationId);
337
+ if (cur === null) throw new Error(`journal 不存在或损坏: ${operationId}`);
338
+ return this.persist(operationId, updater(cur));
339
+ }
340
+
341
+ /** 原子迁移状态(校验合法 transition),并立即持久化。 */
342
+ async transition(operationId: string, to: JournalState): Promise<OperationJournal> {
343
+ return this.update(operationId, (j) => transitionJournalState(j, to));
344
+ }
345
+
346
+ /** 扫描 active/ 下全部 journal op id(只认 <uuid>.json;忽略 tmp/损坏)。 */
347
+ async scanActive(): Promise<string[]> {
348
+ const names = await this.io.readdirNames(this.activeDir());
349
+ const out: string[] = [];
350
+ for (const n of names) {
351
+ if (!isJournalBasename(n)) continue;
352
+ out.push(n.slice(0, -'.json'.length));
353
+ }
354
+ return out;
355
+ }
356
+
357
+ /** 判断某 op 是否已 terminal(读 active 或 completed 的 journal state)。 */
358
+ async isTerminal(operationId: string): Promise<boolean> {
359
+ const j = await this.load(operationId);
360
+ return j !== null && isTerminalState(j.state);
361
+ }
362
+
363
+ async isActive(operationId: string): Promise<boolean> {
364
+ return (await this.loadActive(operationId)) !== null;
365
+ }
366
+
367
+ async terminalStateOf(operationId: string): Promise<JournalState | null> {
368
+ const j = await this.load(operationId);
369
+ return j === null ? null : j.state;
370
+ }
371
+
372
+ /** move active → completed(terminal journal 规整;复用 rename,失败抛错由调用方策略处理)。 */
373
+ async moveToCompleted(operationId: string): Promise<void> {
374
+ if (!isValidOperationId(operationId)) throw new Error(`非法 operationId`);;
375
+ await this.ensureDirs();
376
+ const src = this.activePath(operationId);
377
+ if (!(await this.io.exists(src))) return; // 已不在 active(幂等)
378
+ // 只 move terminal 或已规整的;调用方保证
379
+ const j = await this.loadActive(operationId);
380
+ if (j === null) return;
381
+ if (!isTerminalState(j.state)) {
382
+ throw new Error(`moveToCompleted 仅接受 terminal journal: ${operationId} state=${j.state}`);
383
+ }
384
+ const dst = this.completedPath(operationId);
385
+ if (await this.io.exists(dst)) {
386
+ // 目标已存在(重复规整)→ 删 active 副本(两者内容一致才删;保守:直接覆盖 dst)
387
+ await this.io.rm(src, { force: true });
388
+ return;
389
+ }
390
+ await this.io.rename(src, dst);
391
+ }
392
+
393
+ /** quarantine 损坏/无法 parse/绑定失败的 journal(+ attention sidecar)。幂等。 */
394
+ async quarantine(operationId: string, reason: string): Promise<void> {
395
+ if (!isValidOperationId(operationId)) return; // 非法文件名不 quarantine(避免穿越)
396
+ await this.ensureDirs();
397
+ const src = this.activePath(operationId);
398
+ if (!(await this.io.exists(src))) return;
399
+ const dst = this.quarantinePath(operationId);
400
+ if (await this.io.exists(dst)) {
401
+ // 已 quarantine 过(幂等):移除 active 副本,不重复 move
402
+ await this.io.rm(src, { force: true });
403
+ return;
404
+ }
405
+ await this.io.rename(src, dst);
406
+ await this.io.writeAll(`${dst}${NEEDS_ATTENTION_SIDECAR_SUFFIX}`, `${JSON.stringify({ operationId, reason, at: new Date().toISOString() }, null, 2)}\n`);
407
+ }
408
+
409
+ /** 追加 recovery-history 事件(审计;best-effort 由调用方 try/catch 包裹)。 */
410
+ async appendRecoveryHistory(marker: string, entry: unknown): Promise<void> {
411
+ await this.ensureDirs();
412
+ const name = `${Date.now()}-${crypto.randomBytes(6).toString('hex')}.${marker}.json`;
413
+ await this.io.writeAll(path.join(this.recoveryHistoryDir(), name), `${JSON.stringify(entry, null, 2)}\n`);
414
+ }
415
+
416
+ async listRecoveryHistory(): Promise<string[]> {
417
+ const names = await this.io.readdirNames(this.recoveryHistoryDir());
418
+ return names.sort();
419
+ }
420
+
421
+ /**
422
+ * 收集已被「可恢复 / 未收敛 journal」引用的 snapshotId(Phase 4 F3 prune 保护):
423
+ * 扫描 active/ + quarantine/ 下的 journal,凡 state 非 COMMITTED(即可能仍需 recovery /
424
+ * NEEDS_ATTENTION / ROLLING_BACK / RECOVERY_REQUIRED / 非终态)且 snapshotId 合法
425
+ * → 收集。返回 Set<string>。COMMITTED 的 snapshot 已消费,不保护(可被 retention 淘汰)。
426
+ * 该集合用于 FileSnapshotStore.prune 豁免 —— 引用的 recovery snapshot 绝不可被自动淘汰。
427
+ */
428
+ async listReferencedSnapshotIds(): Promise<Set<string>> {
429
+ const out = new Set<string>();
430
+ const dirs = [this.activeDir(), this.quarantineDir()];
431
+ for (const dir of dirs) {
432
+ for (const name of await this.io.readdirNames(dir)) {
433
+ if (!isJournalBasename(name)) continue;
434
+ const text = await this.io.readFileText(path.join(dir, name)).catch(() => null);
435
+ if (text === null) continue;
436
+ const j = parseSafe(text);
437
+ if (j === null) continue;
438
+ if (j.state === 'COMMITTED') continue; // 已消费,不保护
439
+ if (typeof j.snapshotId === 'string' && j.snapshotId !== '') out.add(j.snapshotId);
440
+ }
441
+ }
442
+ return out;
443
+ }
444
+
445
+ // ---------- SAFE MODE ----------
446
+
447
+ /** 读 SAFE MODE(RECOVERY_REQUIRED / NEEDS_ATTENTION)持久标记。存在且内容为 blocked → true。 */
448
+ async readSafeMode(): Promise<boolean> {
449
+ const p = this.safeModePath();
450
+ if (!(await this.io.exists(p))) return false;
451
+ const text = await this.io.readFileText(p);
452
+ return /blocked|true/i.test(text);
453
+ }
454
+
455
+ /** 写/清 SAFE MODE 标记(atomic)。 */
456
+ async writeSafeMode(blocked: boolean): Promise<void> {
457
+ await this.ensureDirs();
458
+ const p = this.safeModePath();
459
+ if (!blocked) {
460
+ if (await this.io.exists(p)) await this.io.rm(p, { force: true });
461
+ return;
462
+ }
463
+ await this.io.writeAll(p, `${JSON.stringify({ blocked: true, at: new Date().toISOString() }, null, 2)}\n`);
464
+ }
465
+
466
+ // ---------- retention ----------
467
+
468
+ /** 保留策略:completed 保留 N,recovery-history 保留 M。删最旧。幂等。 */
469
+ async retention(completedLimit = 50, historyLimit = 200): Promise<void> {
470
+ const completed = (await this.io.readdirNames(this.completedDir()))
471
+ .filter(isJournalBasename).sort();
472
+ await this.pruneOldest(this.completedDir(), completed, completedLimit);
473
+
474
+ const hist = (await this.io.readdirNames(this.recoveryHistoryDir())).sort();
475
+ await this.pruneOldest(this.recoveryHistoryDir(), hist, historyLimit);
476
+ }
477
+
478
+ private async pruneOldest(dir: string, names: string[], limit: number): Promise<void> {
479
+ if (names.length <= limit) return;
480
+ const toRemove = names.slice(0, names.length - limit);
481
+ for (const n of toRemove) {
482
+ await this.io.rm(path.join(dir, n), { force: true }).catch(() => undefined);
483
+ }
484
+ }
485
+ }
486
+
487
+ // ---------- Environment Fingerprint ----------
488
+
489
+ /**
490
+ * 环境指纹:hash(hostname + 持久化 per-install 随机 token)。
491
+ * token 存 <dataDir>/environment-fingerprint.token(0600,atomic),重启稳定、跨安装不同。
492
+ * 不能用 Date.now/pid/临时 id。
493
+ */
494
+ export async function environmentFingerprint(dataDir: string, io: JournalIo = defaultIo): Promise<string> {
495
+ const tokenPath = path.join(dataDir, 'environment-fingerprint.token');
496
+ let token: string;
497
+ try {
498
+ const tokenDir = path.dirname(tokenPath);
499
+ await io.mkdir(tokenDir, { recursive: true });
500
+ if (await io.exists(tokenPath)) {
501
+ const raw = (await io.readFileText(tokenPath)).trim();
502
+ token = /^[0-9a-f]{32,}$/i.test(raw) ? raw : crypto.randomBytes(24).toString('hex');
503
+ } else {
504
+ token = crypto.randomBytes(24).toString('hex');
505
+ await io.writeAll(tokenPath, `${token}\n`);
506
+ }
507
+ } catch {
508
+ token = crypto.randomBytes(24).toString('hex'); // 读不到 → 新 token(跨启动变化,保守 fallback)
509
+ // 尽力落盘失败不阻塞(本指纹仅用于「是否同环境」,非安全边界)
510
+ }
511
+ const hostname = (() => { try { return os.hostname(); } catch { return 'unknown'; } })();
512
+ return crypto.createHash('sha256').update(`${hostname}|${token}`).digest('hex');
513
+ }
514
+
515
+ /** 生成 operationId(UUID v4,严格 <uuid>.json 形态)。 */
516
+ export function generateOperationId(): string {
517
+ return crypto.randomUUID();
518
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * 本地插件打包的**宿主侧**接线(T1)。
3
+ *
4
+ * 与 `local-plugin-pack.ts` 的分工:
5
+ * - `local-plugin-pack.ts` 是纯逻辑(spec 分类/路径推导/编排),零 node 子进程依赖,可离线单测;
6
+ * - 本文件只负责把真实世界接上:`npm pack` 的 execFile 包装、真实读写、临时目录生命周期。
7
+ * 放在独立文件是为了让 adapter 与 core 引擎都不需要 import `node:child_process`
8
+ * (降低被误打进 client bundle 的风险,且便于测试替身)。
9
+ */
10
+ import fsp from 'node:fs/promises';
11
+ import path from 'node:path';
12
+ import { spawn } from 'node:child_process';
13
+
14
+ import { packLocalPlugins, parsePackOutput } from './local-plugin-pack.ts';
15
+ import { quoteCmdArg } from './plugin-cli.ts';
16
+ import type { PackExec, PackExecResult, PackLocalPluginsResult } from './local-plugin-pack.ts';
17
+ import type { HostContext } from './types.ts';
18
+
19
+ /**
20
+ * 本地源插件打包钩子签名。
21
+ *
22
+ * 刻意在 core 内**重新声明**该签名(而非从 adapters 反向 import):架构边界测试
23
+ * (tests/architecture-boundaries.test.ts F7)规定 core 层只允许依赖
24
+ * node 内置 / core 内部 / schema / utils / security —— core → adapters 是违规方向。
25
+ * adapters 侧的 `LocalPluginPackHook` 与这是**结构等价**的,故可直接互赋(TS 结构化类型)。
26
+ * 注意:这里 host ctx 参数只需 HostContext,无需 adapters 的任何类型。
27
+ */
28
+ export type LocalPluginPackHook = (
29
+ plugins: readonly { name: string; version: string; spec?: string }[],
30
+ ctx: Pick<HostContext, 'profile'>,
31
+ ) => Promise<PackLocalPluginsResult>;
32
+
33
+ /** `npm pack` 单次调用的超时上限(慢网络/大插件留足余量,但不得无限挂起) */
34
+ const PACK_TIMEOUT_MS = 5 * 60 * 1000;
35
+
36
+ /** cmd.exe 垫片路径(Windows 上 npm 是 .cmd;与 core/plugin-cli.ts 的 spawnShim 同源做法) */
37
+ const COMSPEC = process.env.ComSpec ?? 'cmd.exe';
38
+
39
+ /**
40
+ * 默认 exec:把 `npm pack` 作为子进程执行并收集 stdout/stderr。
41
+ *
42
+ * **刻意不用 `execFile(..., { shell: true })`**:该组合会触发 Node 的 DEP0190
43
+ * (`shell:true` + args 数组只做拼接不做转义,存在注入面)。仓库既有解法是
44
+ * 在 Windows 上显式走 `cmd.exe /d /s /c "<转义后的命令行>"`(见 core/plugin-cli.ts
45
+ * 的 spawnShim + quoteCmdArg),本模块复用同一做法。
46
+ *
47
+ * 失败**不 reject**(把退出码交回调用方判定),只有 spawn 本身失败才 reject——
48
+ * 这样「npm 报错」与「npm 不存在」能被分开处理(前者是插件问题,后者是环境问题)。
49
+ */
50
+ const defaultExec: PackExec = (file, args, opts) =>
51
+ new Promise<PackExecResult>((resolve, reject) => {
52
+ const useShim = process.platform === 'win32';
53
+ const [bin, argv] = useShim
54
+ ? [COMSPEC, ['/d', '/s', '/c', `"${[file, ...args].map(quoteCmdArg).join(' ')}"`]]
55
+ : [file, [...args]];
56
+
57
+ const child = spawn(bin, argv, {
58
+ cwd: opts.cwd,
59
+ windowsHide: true,
60
+ windowsVerbatimArguments: useShim,
61
+ shell: false, // 显式关闭:Windows 路径已由 cmd.exe 自行处理
62
+ });
63
+
64
+ let stdout = '';
65
+ let stderr = '';
66
+ let settled = false;
67
+ const timeoutMs = opts.timeoutMs ?? PACK_TIMEOUT_MS;
68
+ const timer = setTimeout(() => {
69
+ if (settled) return;
70
+ settled = true;
71
+ child.kill();
72
+ resolve({ stdout, stderr: `${stderr}\nnpm pack 超时(${timeoutMs}ms)已终止`, code: 1 });
73
+ }, timeoutMs);
74
+ timer.unref?.();
75
+
76
+ child.stdout?.on('data', (d: Buffer | string) => { stdout += String(d); });
77
+ child.stderr?.on('data', (d: Buffer | string) => { stderr += String(d); });
78
+ child.on('error', (err) => {
79
+ if (settled) return;
80
+ settled = true;
81
+ clearTimeout(timer);
82
+ reject(err); // spawn 层失败(ENOENT 等):环境问题,如实上抛
83
+ });
84
+ child.on('close', (code) => {
85
+ if (settled) return;
86
+ settled = true;
87
+ clearTimeout(timer);
88
+ resolve({ stdout, stderr, code });
89
+ });
90
+ });
91
+
92
+ export interface LocalPluginPackHookOptions {
93
+ /** $DSH_HOME 绝对路径 */
94
+ homeDir: string;
95
+ /** 插件数据目录(临时打包目录建在其下,保证与既有目录生命周期一致) */
96
+ dataDir: string;
97
+ /** 覆盖 exec(测试注入) */
98
+ exec?: PackExec;
99
+ }
100
+
101
+ /**
102
+ * 构造注入给 `PluginsAdapter` 的打包钩子。
103
+ *
104
+ * 临时目录建在 `<dataDir>/tmp/`(与既有 tmp 语义一致),构造时不清扫、用完即删:
105
+ * 单次导出内建一个 `<dataDir>/tmp/local-pack-<random>` 目录,导出结束(无论成败)删除,
106
+ * 避免 tarball 残留在用户磁盘上。
107
+ */
108
+ export function createLocalPluginPackHook(opts: LocalPluginPackHookOptions): LocalPluginPackHook {
109
+ const exec = opts.exec ?? defaultExec;
110
+
111
+ return async (plugins, ctx) => {
112
+ // 无本地源插件时零开销返回(不建目录、不 spawn)——由 packLocalPlugins 内部短路,
113
+ // 这里先做便宜的判定避免无谓的 mkdtemp。
114
+ if (!plugins.some((p) => p.spec !== undefined && /^\s*(link|file):/i.test(p.spec))) {
115
+ return { packed: [], rewritten: {}, warnings: [] };
116
+ }
117
+
118
+ const tmpRoot = path.join(opts.dataDir, 'tmp');
119
+ let packDir: string;
120
+ try {
121
+ await fsp.mkdir(tmpRoot, { recursive: true });
122
+ packDir = await fsp.mkdtemp(path.join(tmpRoot, 'local-pack-'));
123
+ } catch (err) {
124
+ return {
125
+ packed: [],
126
+ rewritten: {},
127
+ warnings: [`本地插件打包临时目录创建失败,已跳过本地插件打包: ${err instanceof Error ? err.message : String(err)}`],
128
+ };
129
+ }
130
+
131
+ try {
132
+ // profile 目录:profiles/<profile>(profile 缺省 web,与仓库既有约定一致)
133
+ const profile = ctx.profile !== undefined && ctx.profile !== '' ? ctx.profile : 'web';
134
+ return await packLocalPlugins({
135
+ plugins: plugins.map((p) => ({ name: p.name, version: p.version, spec: p.spec })),
136
+ homeDir: opts.homeDir,
137
+ profileDir: path.join(opts.homeDir, 'profiles', profile),
138
+ packDir,
139
+ exec,
140
+ readFile: (abs) => fsp.readFile(abs),
141
+ mkdir: async (abs) => { await fsp.mkdir(abs, { recursive: true }); },
142
+ timeoutMs: PACK_TIMEOUT_MS,
143
+ });
144
+ } finally {
145
+ // 无论成败都清掉临时目录:tarball 字节已读进内存并随备份走,无需留盘
146
+ await fsp.rm(packDir, { recursive: true, force: true }).catch(() => undefined);
147
+ }
148
+ };
149
+ }
150
+
151
+ /** 供其他模块复用(避免重复实现 npm 输出解析) */
152
+ export { parsePackOutput };