@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,484 @@
1
+ /**
2
+ * Migration History & Auditability Engine(Phase 6)——统一、持久、append-only、可查询、可导出的
3
+ * 迁移/审计历史存储。
4
+ *
5
+ * 职责(严格限定):
6
+ * - migration-history/ 目录下 per-file append-only 条目存储;
7
+ * - 原子写(atomicWriteFile,0600 + symlink-reject);
8
+ * - 读取(只认合法历史文件,忽略 tmp/脏文件;损坏文件跳过并计数);
9
+ * - 查询(kind / 结果 / 时间 / 分区 过滤)、统计、导出(JSON / Markdown);
10
+ * - retention(删最旧、幂等);
11
+ * - **Redaction 边界**:条目内仅有 summary / error 是自由文本,写入前必须经
12
+ * scanAndRedact(含 high-entropy 档)+ redactJournalText 双重清洗;其余字段
13
+ * (kind/result/source/sections/operationId/snapshotId/runId/at)均为常量或 UUID,
14
+ * 无 secret 承载面。
15
+ *
16
+ * 安全/架构纪律(Phase 6 §3):
17
+ * - **不建第二套 framework**:复用 Phase 1 atomicWriteFile、journal 的 per-file append
18
+ * 模式、security 脱敏、paths 保留前缀;本模块是新能力(审计史),非重复基础设施。
19
+ * - **APPEND-ONLY**:条目写入即完整独立文件,无 update/delete API(编译层无暴露);
20
+ * 仅 retention 删最旧。篡改文件 → 读回损坏识别并跳过(不静默接受)。
21
+ * - **DURABLE**:落盘跨重启。
22
+ * - **BOUND**:条目尽量关联 operationId / snapshotId / runId。
23
+ *
24
+ * 与 DSH 运行时解耦:IO 经可注入门面(测试注入失败点),引擎层零 DSH 依赖。
25
+ */
26
+ import fs from 'node:fs/promises';
27
+ import path from 'node:path';
28
+ import crypto from 'node:crypto';
29
+ import { atomicWriteFile } from '../utils/atomic-write.ts';
30
+ import { redact } from '../security/redaction.ts';
31
+ import { scanAndRedact } from '../security/secret-scanner.ts';
32
+ import { sha256Hex } from '../utils/hashing.ts';
33
+
34
+ // ---------- 常量 ----------
35
+
36
+ export const MIGRATION_HISTORY_SCHEMA_VERSION = 1;
37
+ export const MIGRATION_HISTORY_DIR = 'migration-history';
38
+ /** 默认保留上限(retention 删最旧)。 */
39
+ export const DEFAULT_MIGRATION_RETENTION = 1000;
40
+
41
+ /** 文件名正则:`<stamp>-<hex12>.<kind>.json`,只认合法历史文件。 */
42
+ const HISTORY_BASENAME_RE = /^[0-9A-Za-z:.+-]{8,}-[0-9a-f]{12}\.[a-z-]+\.json$/i;
43
+ const TMP_PREFIX = '.dshcm.';
44
+
45
+ /** 合法 kind 集合(§5 COMPLETE 清单 + 评审补 'profile-import')。 */
46
+ const MIGRATION_KINDS = [
47
+ 'import', 'restore', 'rollback',
48
+ 'profile-switch', 'profile-delete', 'profile-rename', 'profile-save', 'profile-import',
49
+ 'sync-apply', 'autosync', 'recovery',
50
+ 'backup', 'snapshot-delete', 'snapshot-prune',
51
+ ] as const;
52
+
53
+ // ---------- 类型 ----------
54
+
55
+ /** §5 覆盖的操作 kind(COMPLETE 不变量;常量枚举,天然非敏感)。 */
56
+ export type MigrationKind = (typeof MIGRATION_KINDS)[number];
57
+
58
+ /** 操作结果。 */
59
+ export type MigrationResult = 'success' | 'failed' | 'skipped';
60
+
61
+ /** 触发来源(常量)。 */
62
+ export type MigrationSource = 'api' | 'autosync' | 'backup-scheduler' | 'recovery' | 'cli' | 'internal';
63
+
64
+ /** 一条不可变的迁移历史记录(磁盘形态;schemaVersion 内联)。 */
65
+ export interface MigrationHistoryEntry {
66
+ schemaVersion: number;
67
+ at: string;
68
+ kind: MigrationKind;
69
+ result: MigrationResult;
70
+ /** 涉及分区 adapter id(常量集合)。 */
71
+ sections: string[];
72
+ /** Journal operation id(UUID,BOUND)。 */
73
+ operationId?: string;
74
+ /** 快照 id(UUID,BOUND)。 */
75
+ snapshotId?: string;
76
+ /** run-registry runId(UUID,BOUND)。 */
77
+ runId?: string;
78
+ source: MigrationSource;
79
+ /** 非敏感摘要(redact + high-entropy 后)。 */
80
+ summary: string;
81
+ /** 失败原因(redact + high-entropy 后)。 */
82
+ error?: string;
83
+ }
84
+
85
+ /**
86
+ * 磁盘形态:schemaVersion + contentHash(对除 contentHash 外全字段的 SHA-256)。
87
+ * contentHash 用于 **append-only 篡改检测**:合法 JSON 内改字段值(含 result/summary/at)
88
+ * → 读回 hash 不符 → 该文件被识别为损坏并跳过(满足「篡改必须被拒绝或检测」)。
89
+ * 仅是内联字段(非 MAC/签名 / 非第二套 integrity framework;审计史非对抗性安全边界)。
90
+ */
91
+ export interface StoredMigrationHistoryEntry extends Omit<MigrationHistoryEntry, 'schemaVersion'> {
92
+ schemaVersion: number;
93
+ contentHash: string;
94
+ }
95
+
96
+ /** 查询条件(纯函数过滤)。 */
97
+ export interface MigrationQuery {
98
+ kinds?: MigrationKind[];
99
+ /** 起始时间(epoch ms,含)。 */
100
+ from?: number;
101
+ /** 结束时间(epoch ms,含)。 */
102
+ to?: number;
103
+ result?: MigrationResult[];
104
+ /** sections 子集匹配(条目 sections 含任一查询分区即命中)。 */
105
+ sections?: string[];
106
+ }
107
+
108
+ /** 统计(纯函数)。 */
109
+ export interface MigrationHistoryStats {
110
+ total: number;
111
+ byKind: Partial<Record<MigrationKind, number>>;
112
+ byResult: Partial<Record<MigrationResult, number>>;
113
+ }
114
+
115
+ export type ExportFormat = 'json' | 'markdown';
116
+
117
+ /** 读取历史的结果:合法条目 + 被识别为损坏/跳过的文件名(append-only 篡改检测)。 */
118
+ export interface ReadMigrationResult {
119
+ entries: StoredMigrationHistoryEntry[];
120
+ /** 被跳过/损坏的文件名(读取时识别,UI 可选警示;不静默接受)。 */
121
+ corrupted: string[];
122
+ }
123
+
124
+ /** 追加写入结果(best-effort 调用方据此判断降级)。 */
125
+ export interface AppendResult {
126
+ ok: boolean;
127
+ entry: StoredMigrationHistoryEntry;
128
+ file?: string;
129
+ error?: string;
130
+ }
131
+
132
+ /** 可注入 IO 门面(测试注入失败点);默认包 node:fs/promises。 */
133
+ export interface MigrationIo {
134
+ mkdir(dir: string, opts: { recursive: boolean }): Promise<void>;
135
+ readdirNames(dir: string): Promise<string[]>;
136
+ readFileText(p: string): Promise<string>;
137
+ writeAll(target: string, content: string): Promise<void>;
138
+ rm(p: string, opts: { recursive?: boolean; force?: boolean }): Promise<void>;
139
+ lstat(p: string): Promise<{ isSymbolicLink(): boolean } | null>;
140
+ exists(p: string): Promise<boolean>;
141
+ }
142
+
143
+ const defaultIo: MigrationIo = {
144
+ async mkdir(d, o) { await fs.mkdir(d, o); },
145
+ async readdirNames(d) { try { return await fs.readdir(d); } catch { return []; } },
146
+ async readFileText(p) { return (await fs.readFile(p, 'utf8')).toString(); },
147
+ async writeAll(t, c) { await atomicWriteFile(t, c, { mode: 0o600, symlink: 'reject' }); },
148
+ async rm(p, o) { await fs.rm(p, o); },
149
+ async lstat(p) { try { return await fs.lstat(p); } catch { return null; } },
150
+ async exists(p) { try { await fs.access(p); return true; } catch { return false; } },
151
+ };
152
+
153
+ // ---------- Redaction(双保险) ----------
154
+
155
+ /** journal 级高熵长 token 掩码(复用 journal 语义,独立实现避免跨层依赖)。 */
156
+ const HIGH_ENTROPY_RE = /([A-Za-z0-9+/_=-]{28,})/g;
157
+
158
+ /** 结构化时间戳/文件名形态:日期段必须由连字符连接(ISO 日期 YYYY-MM-DD 或
159
+ * 紧凑时间戳 YYYYMMDD-HHMMSS)。连字符不存在于 hex/base64 token 中,豁免不会误放行随机密钥。 */
160
+ const DATE_STAMP_RE = /\d{4}-\d{2}-\d{2}|\d{8}-\d{6}/;
161
+
162
+ /** 高熵长 token 掩码(日期戳形态豁免)。 */
163
+ function maskHighEntropy(text: string): string {
164
+ return text.replace(HIGH_ENTROPY_RE, (run) => (DATE_STAMP_RE.test(run) ? run : '[REDACTED]'));
165
+ }
166
+
167
+ /**
168
+ * 历史文本强脱敏:redact(结构化字段 + 已知值形状)+ 高熵长 token 掩码。
169
+ * 用于 summary / error 等可能嵌入任意值的字段(REDACTED 不变量)。
170
+ */
171
+ export function redactHistoryText(text: string): string {
172
+ let out = text;
173
+ try { out = redact(out); } catch { /* 脱敏失败保守处理 */ }
174
+ return maskHighEntropy(out);
175
+ }
176
+
177
+ /**
178
+ * 构造安全条目:对 summary / error 做强脱敏(scanAndRedact 高熵档 + redactHistoryText),
179
+ * 做白名单字段规范化,并计算 contentHash(对除 contentHash 外全字段的 SHA-256)。
180
+ * **历史写入的唯一出口入口**,禁止绕过。
181
+ */
182
+ export function sanitizeEntry(
183
+ raw: Omit<MigrationHistoryEntry, 'schemaVersion'> & { schemaVersion?: number },
184
+ ): StoredMigrationHistoryEntry {
185
+ // 1) 无损子串掩码(redact 值形状 + 高熵长 token),保留周边可读文本——优先,
186
+ // 避免「一个 sk- 子串就整段清空」导致审计可读性全失。
187
+ const masked = {
188
+ summary: redactHistoryText(raw.summary ?? ''),
189
+ error: redactHistoryText(raw.error ?? ''),
190
+ };
191
+ // 2) scanAndRedact(高熵档)作为残留防线:若掩码后仍检测到威胁,保守整值清空,
192
+ // 绝不带敏感值落盘。(scanAndRedact 对裸 string 静默跳过 → 固定包对象。)
193
+ const { sanitized } = scanAndRedact({ summary: masked.summary, error: masked.error }, { highEntropy: true, valuePatterns: true });
194
+ const s = (sanitized ?? {}) as { summary?: unknown; error?: unknown };
195
+ const summary = typeof s.summary === 'string' ? s.summary : '';
196
+ const hasError = ((masked.error ?? '') !== '');
197
+ const error = hasError ? (typeof s.error === 'string' ? s.error : '') : undefined;
198
+ const entry: MigrationHistoryEntry = {
199
+ schemaVersion: MIGRATION_HISTORY_SCHEMA_VERSION,
200
+ at: raw.at,
201
+ kind: raw.kind,
202
+ result: raw.result,
203
+ sections: Array.isArray(raw.sections) ? raw.sections.filter((s): s is string => typeof s === 'string') : [],
204
+ source: sourceGuard(raw.source),
205
+ summary,
206
+ error,
207
+ };
208
+ if (raw.operationId !== undefined && isUuid(raw.operationId)) entry.operationId = raw.operationId;
209
+ if (raw.snapshotId !== undefined && isUuid(raw.snapshotId)) entry.snapshotId = raw.snapshotId;
210
+ if (raw.runId !== undefined && isUuid(raw.runId)) entry.runId = raw.runId;
211
+ const stored = entry as StoredMigrationHistoryEntry;
212
+ stored.contentHash = computeContentHash(entry);
213
+ return stored;
214
+ }
215
+
216
+ const VALID_SOURCES = new Set([
217
+ 'api', 'autosync', 'backup-scheduler', 'recovery', 'cli', 'internal',
218
+ ]);
219
+
220
+ /** 运行期校验 source 属合法枚举;非法回退 'internal'(防未知来源绕过/清洗不一致)。 */
221
+ function sourceGuard(v: unknown): MigrationSource {
222
+ return typeof v === 'string' && VALID_SOURCES.has(v) ? v as MigrationSource : 'internal';
223
+ }
224
+
225
+ /** 对除 contentHash 外全字段计算稳定 SHA-256(字段序稳定、无无关缩进/空白)。 */
226
+ function computeContentHash(entry: MigrationHistoryEntry): string {
227
+ const payload = Object.fromEntries(
228
+ Object.entries(entry).filter(([k]) => k !== 'contentHash'),
229
+ );
230
+ return sha256Hex(canonicalStringify(payload));
231
+ }
232
+
233
+ /** 稳定 JSON 序列化(键排序 + 无多余空白),保证 hash 跨运行稳定。 */
234
+ function canonicalStringify(value: unknown): string {
235
+ return JSON.stringify(sortKeys(value));
236
+ }
237
+
238
+ function sortKeys(v: unknown): unknown {
239
+ if (Array.isArray(v)) return v.map(sortKeys);
240
+ if (v !== null && typeof v === 'object') {
241
+ const out: Record<string, unknown> = {};
242
+ for (const k of Object.keys(v as Record<string, unknown>).sort()) {
243
+ out[k] = sortKeys((v as Record<string, unknown>)[k]);
244
+ }
245
+ return out;
246
+ }
247
+ return v;
248
+ }
249
+
250
+ function isUuid(v: unknown): v is string {
251
+ return typeof v === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(v);
252
+ }
253
+
254
+ /** 合法 kind 判定(类型守卫 + 运行时校验)。 */
255
+ export function isValidMigrationKind(k: unknown): k is MigrationKind {
256
+ return typeof k === 'string' && (MIGRATION_KINDS as readonly string[]).includes(k);
257
+ }
258
+
259
+ /** 判断文件 basename 是否是可追踪历史文件(<sortable>-<hex>.json)。忽略 tmp 及其它。 */
260
+ export function isHistoryBasename(name: string): boolean {
261
+ if (!HISTORY_BASENAME_RE.test(name)) return false;
262
+ return !name.startsWith(TMP_PREFIX);
263
+ }
264
+
265
+ /** 文件名安全:仅 basename(不含目录分隔符/穿越)。 */
266
+ export function isSafeHistoryFilename(name: string): boolean {
267
+ if (name === '' || name.includes('/') || name.includes('\\') || name.includes('\0')) return false;
268
+ return name !== '.' && name !== '..';
269
+ }
270
+
271
+ /**
272
+ * 生成排序文件名(Windows-safe 定宽时间戳 + 随机后缀 + kind 标记)。
273
+ *
274
+ * 时间戳契约(Windows/排序可靠性):`toISOString().replace(/[:Z]/g,'')`
275
+ * → `2026-08-30T120000.123`(UTC 定宽、仅 ASCII、无 Windows 非法字符 `<>:"/\|?*`、
276
+ * 字典序== 时间序)。**禁止**以原始 `toISOString()`(含 `:`)直接落文件名(Windows rename 失败)。
277
+ * 与既有 `dateStamp`(`YYYYMMDD-HHmmss`)语义同为「定宽 + 仅 ASCII + 可排序」,本实现用 UTC 含毫秒
278
+ * 以获得更高时序精度;排序可靠性一致。
279
+ */
280
+ export function makeHistoryFilename(now: Date, kind: MigrationKind): string {
281
+ const stamp = now.toISOString().replace(/[:Z]/g, '');
282
+ const rand = crypto.randomBytes(6).toString('hex');
283
+ return `${stamp}-${rand}.${kind}.json`;
284
+ }
285
+
286
+ // ---------- 解析 ----------
287
+
288
+ function parseEntry(text: string): StoredMigrationHistoryEntry | null {
289
+ try {
290
+ const p = JSON.parse(text) as StoredMigrationHistoryEntry;
291
+ if (p === null || typeof p !== 'object') return null;
292
+ if (p.schemaVersion !== MIGRATION_HISTORY_SCHEMA_VERSION) return null;
293
+ if (!isValidMigrationKind(p.kind)) return null;
294
+ if (typeof p.at !== 'string' || p.at === '') return null;
295
+ if (!['success', 'failed', 'skipped'].includes(p.result)) return null;
296
+ if (typeof p.source !== 'string') return null;
297
+ if (typeof p.summary !== 'string') return null;
298
+ // 可选字段校验
299
+ if (p.operationId !== undefined && !isUuid(p.operationId)) return null;
300
+ if (p.snapshotId !== undefined && !isUuid(p.snapshotId)) return null;
301
+ if (p.runId !== undefined && !isUuid(p.runId)) return null;
302
+ if (p.sections !== undefined && (!Array.isArray(p.sections) || p.sections.some((s) => typeof s !== 'string'))) return null;
303
+ if (p.error !== undefined && typeof p.error !== 'string') return null;
304
+ // **append-only 篡改检测**:contentHash 必须匹配(对除 contentHash 外全字段)。
305
+ // 合法 JSON 内改任意字段值 → hash 不符 → 拒绝(计入 corrupted,不静默接受)。
306
+ if (typeof p.contentHash !== 'string' || p.contentHash === '') return null;
307
+ const payload = Object.fromEntries(Object.entries(p).filter(([k]) => k !== 'contentHash'));
308
+ if (sha256Hex(canonicalStringify(payload)) !== p.contentHash) return null;
309
+ const { contentHash, ...rest } = p;
310
+ return { ...rest, contentHash } as StoredMigrationHistoryEntry;
311
+ } catch {
312
+ return null;
313
+ }
314
+ }
315
+
316
+ // ---------- IO ----------
317
+
318
+ export interface MigrationStoreOptions {
319
+ /** history 存放目录(<dataDir>/migration-history)。 */
320
+ dir: string;
321
+ io?: MigrationIo;
322
+ /** 保留上限(retention 用;调用方可覆盖)。 */
323
+ retention?: number;
324
+ }
325
+
326
+ /** MigrationHistory:per-file append-only 历史存储。 */
327
+ export class MigrationStore {
328
+ private readonly dir: string;
329
+ private readonly io: MigrationIo;
330
+
331
+ constructor(opts: MigrationStoreOptions) {
332
+ this.dir = opts.dir;
333
+ this.io = opts.io ?? defaultIo;
334
+ }
335
+
336
+ /** 追加一条历史(原子写)。best-effort:调用方须经 tryAppendHistory 处理失败降级。 */
337
+ async append(raw: Omit<MigrationHistoryEntry, 'schemaVersion' | 'at' | 'kind'> & { kind: MigrationKind; at?: string }): Promise<AppendResult> {
338
+ try {
339
+ const at = raw.at ?? new Date().toISOString();
340
+ const entry = sanitizeEntry({ ...raw, at } as Omit<MigrationHistoryEntry, 'schemaVersion'>);
341
+ // **防覆盖**:目标已存在(同毫秒+同随机后缀碰撞,概率极低但非零)→ 换名,
342
+ // 绝不覆盖既有条目(APPEND-ONLY 硬不变量:条目一旦写入不可改)。
343
+ let filename = makeHistoryFilename(new Date(at), entry.kind);
344
+ let target = path.join(this.dir, filename);
345
+ await this.io.mkdir(this.dir, { recursive: true });
346
+ let guard = 0;
347
+ while (await this.io.exists(target)) {
348
+ if (++guard > 8) return { ok: false, entry, error: 'history filename collision (retry 上限)' };
349
+ filename = makeHistoryFilename(new Date(at + guard), entry.kind);
350
+ target = path.join(this.dir, filename);
351
+ }
352
+ await this.io.writeAll(target, `${JSON.stringify(entry, null, 2)}\n`);
353
+ return { ok: true, entry, file: filename };
354
+ } catch (error) {
355
+ const entry = sanitizeEntry({ ...raw, at: raw.at ?? new Date().toISOString() } as Omit<MigrationHistoryEntry, 'schemaVersion'>);
356
+ return { ok: false, entry, error: error instanceof Error ? error.message : String(error) };
357
+ }
358
+ }
359
+
360
+ /** 读取全部合法历史条目(旧→新;损坏/非史文件跳过并计数)。 */
361
+ async read(): Promise<ReadMigrationResult> {
362
+ const names = await this.io.readdirNames(this.dir).catch(() => []);
363
+ const entries: StoredMigrationHistoryEntry[] = [];
364
+ const corrupted: string[] = [];
365
+ const sorted = names.filter(isHistoryBasename).sort();
366
+ for (const name of sorted) {
367
+ const p = path.join(this.dir, name);
368
+ let text: string;
369
+ try {
370
+ if ((await this.io.lstat(p))?.isSymbolicLink() === true) { corrupted.push(name); continue; } // symlink 防御
371
+ text = await this.io.readFileText(p);
372
+ } catch {
373
+ corrupted.push(name);
374
+ continue;
375
+ }
376
+ const parsed = parseEntry(text);
377
+ if (parsed === null) { corrupted.push(name); continue; }
378
+ entries.push(parsed);
379
+ }
380
+ return { entries, corrupted };
381
+ }
382
+
383
+ /**
384
+ * 保留清理:按文件名排序删除最旧合法历史文件,幂等。只删合法历史文件(防误删)。
385
+ * 返回本次删除的文件名。
386
+ */
387
+ async retention(limit = DEFAULT_MIGRATION_RETENTION): Promise<string[]> {
388
+ const names = (await this.io.readdirNames(this.dir).catch(() => [])).filter(isHistoryBasename).sort();
389
+ const removed: string[] = [];
390
+ if (names.length <= limit) return removed;
391
+ const toRemove = names.slice(0, names.length - limit);
392
+ for (const name of toRemove) {
393
+ try {
394
+ await this.io.rm(path.join(this.dir, name), { force: true });
395
+ removed.push(name);
396
+ } catch { /* 单文件删失败不中断其余 */ }
397
+ }
398
+ return removed;
399
+ }
400
+ }
401
+
402
+ // ---------- 查询 / 统计 / 导出(纯函数) ----------
403
+
404
+ /** 纯函数过滤(无 IO)。 */
405
+ export function queryHistory(entries: StoredMigrationHistoryEntry[], q: MigrationQuery): StoredMigrationHistoryEntry[] {
406
+ return entries.filter((e) => {
407
+ if (q.kinds !== undefined && q.kinds.length > 0 && !q.kinds.includes(e.kind)) return false;
408
+ if (q.result !== undefined && q.result.length > 0 && !q.result.includes(e.result)) return false;
409
+ if (q.sections !== undefined && q.sections.length > 0) {
410
+ if (!e.sections.some((s) => q.sections!.includes(s))) return false;
411
+ }
412
+ const t = Date.parse(e.at);
413
+ if (!Number.isNaN(t)) {
414
+ if (q.from !== undefined && t < q.from) return false;
415
+ if (q.to !== undefined && t > q.to) return false;
416
+ }
417
+ return true;
418
+ });
419
+ }
420
+
421
+ /** 纯函数统计(kind/result 计数)。 */
422
+ export function summarizeHistory(entries: StoredMigrationHistoryEntry[]): MigrationHistoryStats {
423
+ const stats: MigrationHistoryStats = { total: entries.length, byKind: {}, byResult: {} };
424
+ for (const e of entries) {
425
+ stats.byKind[e.kind] = (stats.byKind[e.kind] ?? 0) + 1;
426
+ stats.byResult[e.result] = (stats.byResult[e.result] ?? 0) + 1;
427
+ }
428
+ return stats;
429
+ }
430
+
431
+ /** 导出报告(纯函数)。渲染文本统一过 redact() 兜底(安全不变量)。 */
432
+ export function renderExport(entries: StoredMigrationHistoryEntry[], format: ExportFormat, locale: 'zh' | 'en' = 'zh'): string {
433
+ const L = locale === 'zh'
434
+ ? { title: 'DSH 配置迁移历史', total: '总数', empty: '暂无迁移记录', kind: '操作', time: '时间', result: '结果', sections: '分区', detail: '摘要' }
435
+ : { title: 'DSH Config Migration History', total: 'Total', empty: 'No migration records', kind: 'Operation', time: 'Time', result: 'Result', sections: 'Sections', detail: 'Summary' };
436
+ const resultLabel = (r: MigrationResult): string => r === 'success' ? (locale === 'zh' ? '成功' : 'success') : r === 'failed' ? (locale === 'zh' ? '失败' : 'failed') : (locale === 'zh' ? '跳过' : 'skipped');
437
+
438
+ if (format === 'json') {
439
+ const stats = summarizeHistory(entries);
440
+ return redact(JSON.stringify({ title: L.title, stats, entries }, null, 2));
441
+ }
442
+
443
+ if (entries.length === 0) return `# ${L.title}\n\n${L.empty}\n`;
444
+ const lines: string[] = [];
445
+ lines.push(`# ${L.title}`, '');
446
+ lines.push(`> ${L.total}: ${entries.length}`, '');
447
+ lines.push('| ' + [L.time, L.kind, L.result, L.sections, L.detail].join(' | ') + ' |');
448
+ lines.push('|' + ' --- |'.repeat(5));
449
+ for (const e of entries) {
450
+ const detail = redact(e.summary + (e.error !== undefined ? ` — ${e.error}` : ''));
451
+ lines.push(`| ${e.at} | ${e.kind} | ${resultLabel(e.result)} | ${redact(e.sections.join(', '))} | ${detail} |`);
452
+ }
453
+ lines.push('');
454
+ return lines.join('\n');
455
+ }
456
+
457
+ /** 解析查询参数为 MigrationQuery(宿主路由复用;过滤非法输入)。 */
458
+ export function parseHistoryQuery(raw: Record<string, unknown>): MigrationQuery {
459
+ const q: MigrationQuery = {};
460
+ const kinds = parseList(raw['kind']);
461
+ if (kinds.length > 0) q.kinds = kinds.filter(isValidMigrationKind);
462
+ const results = parseList(raw['result']);
463
+ if (results.length > 0) q.result = results.filter((r): r is MigrationResult => r === 'success' || r === 'failed' || r === 'skipped');
464
+ const sections = parseList(raw['sections']);
465
+ if (sections.length > 0) q.sections = sections;
466
+ const from = parseNum(raw['from']);
467
+ const to = parseNum(raw['to']);
468
+ if (from !== undefined) q.from = from;
469
+ if (to !== undefined) q.to = to;
470
+ return q;
471
+ }
472
+
473
+ function parseList(v: unknown): string[] {
474
+ if (v === undefined) return [];
475
+ if (typeof v === 'string') return v.split(',').map((s) => s.trim()).filter((s) => s !== '');
476
+ if (Array.isArray(v)) return v.filter((x): x is string => typeof x === 'string' && x !== '');
477
+ return [];
478
+ }
479
+
480
+ function parseNum(v: unknown): number | undefined {
481
+ if (typeof v === 'string' && v !== '' && !Number.isNaN(Number(v))) return Number(v);
482
+ if (typeof v === 'number' && !Number.isNaN(v)) return v;
483
+ return undefined;
484
+ }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * model-tools 编排测试:2 个 Agent 模型工具的纯编排函数(createModelTools)+ 注册层(registerModelTools)。
3
+ * - config_sync_push:复用 SyncEngine.push(内存 transport),返回 snapshotId
4
+ * - config_sync_pull:空远端 → 零写入空差异(不改本地配置)
5
+ * - registerModelTools:tools 存在注册 2 工具;tools 缺失静默跳过
6
+ * 对齐 sync-engine.test.ts:真实 tmp 目录 + makeContext + createAdapters + 内存 transport。
7
+ */
8
+ import test from 'node:test'
9
+ import assert from 'node:assert/strict'
10
+ import fs from 'node:fs/promises'
11
+ import os from 'node:os'
12
+ import path from 'node:path'
13
+
14
+ import type { Context } from '@deepseek-ai/cordis'
15
+ import { createModelTools, registerModelTools, type ModelToolsDeps } from './model-tools.ts'
16
+ import { makeContext, MemSnapshotStore } from '../adapters/test-helpers.ts'
17
+ import { createAdapters } from '../adapters/index.ts'
18
+ import { SyncEngine } from '../sync/sync-engine.ts'
19
+ import { Importer } from './importer.ts'
20
+ import { writeSyncConfig } from '../sync/sync-config.ts'
21
+ import type { SyncConfig } from '../sync/sync-config.ts'
22
+ import type { SyncTransport, SyncSnapshot, SyncSnapshotMeta } from '../sync/transport.ts'
23
+ import { computeSnapshotMeta } from '../sync/transport.ts'
24
+ import { EnvironmentLockUnavailableError } from '../utils/env-lock.ts'
25
+ import type { MutationLockPort } from '../utils/env-lock.ts'
26
+ import { createSecretScanner } from '../security/secret-scanner.ts'
27
+ import type { SecretScanner } from './types.ts'
28
+
29
+ /** 内存 SyncTransport(spy:记录方法调用,供断言「pull 不写远端」)。 */
30
+ class MemSyncTransport implements SyncTransport {
31
+ readonly type = 'memory'
32
+ snapshots = new Map<string, SyncSnapshot>()
33
+ metas: SyncSnapshotMeta[] = []
34
+ calls: string[] = []
35
+ async list(): Promise<SyncSnapshotMeta[]> {
36
+ this.calls.push('list')
37
+ return [...this.metas].sort((a, b) => (a.createdAt < b.createdAt ? -1 : 1))
38
+ }
39
+ async upload(snapshot: SyncSnapshot): Promise<SyncSnapshotMeta> {
40
+ this.calls.push('upload')
41
+ this.snapshots.set(snapshot.id, snapshot)
42
+ this.metas.push(computeSnapshotMeta(snapshot))
43
+ return computeSnapshotMeta(snapshot)
44
+ }
45
+ async download(id: string): Promise<SyncSnapshot> {
46
+ this.calls.push('download')
47
+ const s = this.snapshots.get(id)
48
+ if (!s) throw new Error(`快照不存在: ${id}`)
49
+ return s
50
+ }
51
+ async delete(id: string): Promise<void> {
52
+ this.calls.push('delete')
53
+ this.snapshots.delete(id)
54
+ this.metas = this.metas.filter((m) => m.id !== id)
55
+ }
56
+ }
57
+
58
+ const NS = ['general', 'theme']
59
+
60
+ function seedSettings(ctx: ReturnType<typeof makeContext>): void {
61
+ ctx.settings.ns.set('general', { value: { theme: 'dark', language: 'zh-CN' }, revision: 3, secrets: [] })
62
+ ctx.settings.ns.set('theme', { value: { mode: 'dark' }, revision: 1, secrets: [] })
63
+ }
64
+
65
+ /** 构造 model-tools deps:真实 tmp 目录 + 内存 SyncTransport。 */
66
+ async function makeDeps(opts: {
67
+ transport?: MemSyncTransport
68
+ } = {}): Promise<{ deps: ModelToolsDeps; tmp: string; transport: MemSyncTransport; ctx: ReturnType<typeof makeContext> }> {
69
+ const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'dsh-model-tools-'))
70
+ const ctx = makeContext('win32', path.join(tmp, 'home'))
71
+ seedSettings(ctx)
72
+ const adapters = createAdapters({ namespaces: NS })
73
+ const syncDir = path.join(tmp, 'sync')
74
+ const transport = opts.transport ?? new MemSyncTransport()
75
+ const makeSyncEngine = (cfg: SyncConfig): SyncEngine => {
76
+ const importer = new Importer({ ctx, adapters, snapshotStore: new MemSnapshotStore() })
77
+ return new SyncEngine({
78
+ ctx,
79
+ transport,
80
+ stateDir: syncDir,
81
+ localSnapshotsDir: path.join(syncDir, 'snapshots'),
82
+ zipDir: path.join(tmp, 'tmp'),
83
+ adapters,
84
+ importer,
85
+ now: () => new Date('2026-08-16T12:00:00.000Z'),
86
+ })
87
+ }
88
+ return {
89
+ deps: {
90
+ host: ctx,
91
+ adapters,
92
+ syncDir,
93
+ makeSyncEngine,
94
+ },
95
+ tmp,
96
+ transport,
97
+ ctx,
98
+ }
99
+ }
100
+
101
+ test('config_sync_push:复用 SyncEngine.push(内存 transport),返回 snapshotId', async () => {
102
+ const { deps, tmp, transport } = await makeDeps()
103
+ try {
104
+ await writeSyncConfig(deps.syncDir, { schemaVersion: 2, transport: 'git', git: { repoUrl: 'https://github.com/u/r.git' } })
105
+ const tools = createModelTools(deps)
106
+ const out = (await tools.syncPush({ channel: 'git' })) as { ok: boolean; snapshotId: string; sections: string[] }
107
+ assert.equal(out.ok, true)
108
+ assert.ok(out.snapshotId !== '')
109
+ assert.ok(out.sections.length > 0, 'portable 分区进入同步')
110
+ assert.ok(transport.calls.includes('upload'), 'push 写远端')
111
+ const uploadedSections = transport.snapshots.get(out.snapshotId)!.sections as Record<string, unknown>
112
+ assert.ok(!('secrets' in uploadedSections), '不含 secrets 分区')
113
+ // credentialsStatus 只含 configured/source 标记(值恒不导出),可勾选同步
114
+ assert.ok('credentialsStatus' in uploadedSections, 'credentialsStatus 状态标记进入同步')
115
+ } finally {
116
+ await fs.rm(tmp, { recursive: true, force: true })
117
+ }
118
+ })
119
+
120
+ test('config_sync_pull:空远端 → 零写入空差异', async () => {
121
+ const { deps, tmp, transport, ctx } = await makeDeps()
122
+ try {
123
+ await writeSyncConfig(deps.syncDir, { schemaVersion: 2, transport: 'git', git: { repoUrl: 'https://github.com/u/r.git' } })
124
+ await ctx.fs.writeFile('settings.yaml', Buffer.from('foo: bar', 'utf8'))
125
+ const before = await ctx.fs.listRecursive('')
126
+ const tools = createModelTools(deps)
127
+ const out = (await tools.syncPull({ channel: 'git' })) as { ok: boolean; snapshotId: string; changes: unknown[]; needsReview: boolean }
128
+ assert.equal(out.ok, true)
129
+ assert.equal(out.snapshotId, '')
130
+ assert.deepEqual(out.changes, [], '空远端 → 无差异')
131
+ // pull 零写入:本地文件集合不变
132
+ assert.deepEqual(await ctx.fs.listRecursive(''), before)
133
+ assert.ok(!transport.calls.includes('upload'), 'pull 不写远端')
134
+ } finally {
135
+ await fs.rm(tmp, { recursive: true, force: true })
136
+ }
137
+ })
138
+
139
+ // ------------------------------------------------ 注册层(registerModelTools)
140
+
141
+ /** 最小 mock Cordis ctx:tools 服务可选 + effect 收集 disposer(不跑副作用)。 */
142
+ function mockCtx(toolsSvc: unknown): { ctx: Context; registered: string[]; effects: (() => void)[] } {
143
+ const registered: string[] = []
144
+ const effects: (() => void)[] = []
145
+ const ctx = {
146
+ get: (name: string) => (name === 'tools' ? toolsSvc : undefined),
147
+ tools: toolsSvc === null || toolsSvc === undefined
148
+ ? undefined
149
+ : { register: (def: { name: string }) => { registered.push(def.name); return () => {} } },
150
+ effect: (cb: () => void) => { effects.push(cb) },
151
+ } as unknown as Context
152
+ return { ctx, registered, effects }
153
+ }
154
+
155
+ test('registerModelTools:tools 服务存在 → 注册 2 个工具(走 ctx.get 结果,属性访问守卫不崩)', async () => {
156
+ const { deps, tmp } = await makeDeps()
157
+ try {
158
+ const names: string[] = []
159
+ const toolsSvc = { register: (def: { name: string }) => { names.push(def.name); return () => {} } }
160
+ // 模拟真实 Cordis ctx:ctx.get('tools') 返回服务,但 ctx.tools 属性访问因插件未声明
161
+ // inject: ['tools'] 而抛 "cannot get property without inject"(回归:注册必须走 get
162
+ // 结果而非属性——旧实现 ctx.tools.register 在这里会崩溃)。
163
+ const wrapped = {
164
+ get: (name: string) => (name === 'tools' ? toolsSvc : undefined),
165
+ effect: () => {},
166
+ } as unknown as Context
167
+ Object.defineProperty(wrapped, 'tools', {
168
+ get() { throw new Error('cannot get property "tools" without inject') },
169
+ })
170
+ // 不抛错 = 注册走 toolsSvc + defineTool schema 全部可编译(无效 schema 会在定义时抛错)
171
+ registerModelTools(wrapped, deps)
172
+ assert.deepEqual(names.sort(), ['config_sync_pull', 'config_sync_push'].sort())
173
+ } finally {
174
+ await fs.rm(tmp, { recursive: true, force: true })
175
+ }
176
+ })
177
+
178
+ test('registerModelTools:tools 服务缺失 → 静默跳过(不抛错、不注册)', async () => {
179
+ const { deps, tmp } = await makeDeps()
180
+ try {
181
+ const wrapped = {
182
+ get: () => null,
183
+ effect: () => {},
184
+ } as unknown as Context
185
+ registerModelTools(wrapped, deps)
186
+ assert.ok(true, '不抛错即通过')
187
+ } finally {
188
+ await fs.rm(tmp, { recursive: true, force: true })
189
+ }
190
+ })