@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,934 @@
1
+ /**
2
+ * 三段式核心(对齐设计 §5.1/§13.5):
3
+ * analyzeImport() → ImportAnalysis(纯计算,零写入)
4
+ * createImportPlan() → ImportPlan(汇总 PlanItem + 冲突决策 + 路径映射;纯计算)
5
+ * executeImportPlan() → ImportResult(快照 → 分阶段 apply → 校验 → 结果/回滚)
6
+ *
7
+ * Dry Run 保证:analyzeImport / createImportPlan 除读取 ZIP 字节外不做任何系统修改,
8
+ * ZIP 以内存方式解析(不进磁盘),杜绝不可信输入在分析阶段落盘。
9
+ */
10
+ import fs from 'node:fs/promises';
11
+ import path from 'node:path';
12
+ import { sha256Hex } from '../utils/hashing.js';
13
+ import { parseJsonSafe } from '../utils/json.js';
14
+ import { SECTION_FILE_PREFIXES, SECTION_IDS, SECTION_JSON_PATHS, isFileSection, validateSectionData, } from '../schema/config.js';
15
+ import { CHECKSUMS_FILE, MANIFEST_FILE, parseManifest, validateManifest } from '../schema/manifest.js';
16
+ import { CURRENT_SCHEMA_VERSION, canImport, describeVersion, isSupported, isTooNew, needsMigration, } from '../schema/versions.js';
17
+ import { migrateToCurrent } from '../migrations/index.js';
18
+ import { isAbsolutePath, applyPrefixMappings } from '../utils/paths.js';
19
+ import { parseZip } from '../utils/zip.js';
20
+ import { loadTombstones, isTombstoned } from '../schema/tombstones.js';
21
+ import { DEFAULT_SENSITIVE_RELS, restoreVaultFiles } from '../security/vault.js';
22
+ import { createSnapshot, resolveFileTarget, resolveFileTargetRel } from './backup.js';
23
+ import { rollback } from './rollback.js';
24
+ import { computeCompatibility } from './validator.js';
25
+ import { msgOf } from './messages.js';
26
+ import { ImportNotConfirmedError, ImportUserSkippedError, } from './types.js';
27
+ /** 执行阶段顺序(设计 §5.4:副作用大的 patch/安装最后) */
28
+ const APPLY_ORDER = [
29
+ 'settings', 'ui', 'providers', 'prompts', 'skills', 'agentPresets',
30
+ 'agentInstructions', 'workspaces', 'pluginFiles', 'mcp', 'plugins', 'credentialsStatus',
31
+ // P1-1 修复:self 分区(插件自身配置 sync/market/ui-prefs)此前不在 APPLY_ORDER,
32
+ // 导致 plan 含 self 项但执行循环跳过、导入时被静默丢弃。加入执行顺序末尾(低风险配置文件)。
33
+ 'self',
34
+ ];
35
+ /** ZIP 内可执行文件扩展名黑名单(§19.6:只警告,本插件不执行任何脚本) */
36
+ const EXECUTABLE_EXTENSIONS = new Set(['.exe', '.bat', '.cmd', '.sh', '.ps1', '.dll', '.so', '.dylib', '.bin', '.jar']);
37
+ /** 已知外部依赖(MCP command 检测用,§15) */
38
+ const KNOWN_DEPENDENCIES = new Set([
39
+ 'npx', 'node', 'npm', 'pnpm', 'yarn', 'bun', 'python', 'python3', 'pip', 'pip3',
40
+ 'uv', 'git', 'docker', 'rg', 'bash', 'zsh', 'code', 'cargo', 'go',
41
+ ]);
42
+ export class Analyzer {
43
+ ctx;
44
+ adapters;
45
+ snapshotStore;
46
+ limits;
47
+ dependencyChecker;
48
+ parseZipFn;
49
+ msg;
50
+ /** 会话内 bundle 缓存(zipPath → 解析结果),避免重复解压 */
51
+ bundleCache = new Map();
52
+ constructor(opts) {
53
+ this.ctx = opts.ctx;
54
+ this.adapters = opts.adapters;
55
+ this.snapshotStore = opts.snapshotStore;
56
+ this.limits = opts.limits;
57
+ this.dependencyChecker = opts.dependencyChecker;
58
+ this.parseZipFn = opts.parseZipOverride ?? parseZip;
59
+ this.msg = opts.msg ?? msgOf(opts.ctx);
60
+ }
61
+ /* ---------------- 第 1-6 步:ZIP 读入 → 安全解析 → manifest → 完整性 → schema ---------------- */
62
+ async loadBundle(zipPath) {
63
+ const cached = this.bundleCache.get(zipPath);
64
+ if (cached)
65
+ return cached;
66
+ // 1. 选 ZIP(存在性)
67
+ let raw;
68
+ try {
69
+ raw = await fs.readFile(zipPath);
70
+ }
71
+ catch (err) {
72
+ throw new Error(this.msg('import.readFailed', { zip: zipPath, reason: err instanceof Error ? err.message : String(err) }));
73
+ }
74
+ // 2. 校验 ZIP(安全解析:条目名/数量/体积上限)
75
+ const archive = this.parseZipFn(raw, this.limits);
76
+ // 3. manifest
77
+ if (!archive.has(MANIFEST_FILE))
78
+ throw new Error(this.msg('import.noManifest'));
79
+ let manifest;
80
+ try {
81
+ manifest = parseManifest(archive.readEntryText(MANIFEST_FILE));
82
+ }
83
+ catch (err) {
84
+ throw new Error(this.msg('import.manifestParseFailed', { reason: err instanceof Error ? err.message : String(err) }));
85
+ }
86
+ // 4. 完整性(integrity/checksums.json 逐一 SHA-256)
87
+ const zipWarnings = [];
88
+ let checksums = { ok: true, mismatches: [], missing: [] };
89
+ // 4a. H2:校验表**缺失**或**为空**时,全部条目都处于「未登记 ⇒ 未被校验」状态。
90
+ // 此前整段完整性逻辑(含下面的反向检查)都嵌在 `if (archive.has(CHECKSUMS_FILE))` 内,
91
+ // 于是「剥掉 checksums.json」或「把它置为 {}」= 一个条目都不校验,却 valid=true / 零告警。
92
+ // 两种形态对用户是同一件事(没有任何条目被校验),故共用同一条告警,不制造近重复文案。
93
+ const table = archive.has(CHECKSUMS_FILE)
94
+ ? parseJsonSafe(archive.readEntryText(CHECKSUMS_FILE))
95
+ : null;
96
+ if (table === null || Object.keys(table).length === 0) {
97
+ zipWarnings.push(this.msg('import.checksumsMissing'));
98
+ }
99
+ if (table !== null && Object.keys(table).length > 0) {
100
+ const entries = new Map();
101
+ for (const name of archive.names()) {
102
+ if (name === MANIFEST_FILE || name === CHECKSUMS_FILE)
103
+ continue;
104
+ try {
105
+ entries.set(name, archive.readEntry(name));
106
+ }
107
+ catch {
108
+ entries.delete(name); // 损坏条目在完整性阶段即失败
109
+ checksums.ok = false;
110
+ checksums.mismatches.push(name);
111
+ }
112
+ }
113
+ const result = await verifyAgainstTable(entries, table);
114
+ checksums = result;
115
+ if (!checksums.ok) {
116
+ throw new Error(this.msg('import.integrityFailed', {
117
+ entries: [...checksums.mismatches, ...checksums.missing].map((m) => `"${m}"`).join(', '),
118
+ }));
119
+ }
120
+ // 4b. G-04:反向完整性——ZIP 内**不在**校验表里的条目(未登记 ⇒ 未被校验)。
121
+ // verifyAgainstTable 只遍历表里的键,所以 ZIP 里多出的条目此前既不校验也不告知;
122
+ // 这里把它变成显式 warning(不阻断:未知 ZIP 条目不参与校验是格式 v1 的既定语义)。
123
+ // 排除 manifest.json / checksums.json 自身与目录条目(以 "/" 结尾)。
124
+ const tableKeys = new Set(Object.keys(table));
125
+ const extraEntries = archive.names().filter((name) => name !== MANIFEST_FILE && name !== CHECKSUMS_FILE
126
+ && !name.endsWith('/') && !tableKeys.has(name));
127
+ if (extraEntries.length > 0) {
128
+ zipWarnings.push(this.msg('import.extraEntries', {
129
+ entries: extraEntries.map((e) => `"${e}"`).join(', '),
130
+ }));
131
+ }
132
+ }
133
+ // 5. schema 版本判定(集中):过新(isTooNew)或过旧(低于 MIN_SUPPORTED)→ 一律硬失败(行为不变)
134
+ if (isTooNew(manifest.schemaVersion) || !isSupported(manifest.schemaVersion)) {
135
+ throw new Error(this.msg('import.schemaUnsupported', { version: describeVersion(manifest.schemaVersion) }));
136
+ }
137
+ // 5b. G-06:把「可迁移的旧 schema」真正沿迁移链升级——此前 `migrateToCurrent` 在 src/ 内零引用,
138
+ // 迁移链有定义、有单测、却从未执行。迁移结果 doc 成为后续 manifest,并重新校验其合法性。
139
+ // 注:当前 MIN_SUPPORTED = CURRENT = 1 ⇒ needsMigration 恒假 ⇒ 此处零迁移(旧 v1 行为不变);
140
+ // 一旦 CURRENT > MIN,旧备份会在这里被真实迁移,而不是「判定可迁移却按新格式直接用」。
141
+ const migrationWarnings = [];
142
+ if (needsMigration(manifest.schemaVersion)) {
143
+ const migrated = runSchemaMigration(manifest, manifest.schemaVersion, CURRENT_SCHEMA_VERSION, this.msg);
144
+ manifest = migrated.manifest;
145
+ migrationWarnings.push(...migrated.warnings);
146
+ }
147
+ // 6. 扫描:可执行文件条目 → 警告(不执行)
148
+ for (const name of archive.names()) {
149
+ const ext = name.slice(name.lastIndexOf('.')).toLowerCase();
150
+ if (EXECUTABLE_EXTENSIONS.has(ext)) {
151
+ zipWarnings.push(this.msg('import.executableWarning', { name }));
152
+ }
153
+ }
154
+ const bundle = { archive, manifest, checksums, zipWarnings, migrationWarnings };
155
+ this.bundleCache.set(zipPath, bundle);
156
+ return bundle;
157
+ }
158
+ /* ---------------- 第 7 步:分区内容扫描/提取 ---------------- */
159
+ extractSections(bundle) {
160
+ const { archive, manifest } = bundle;
161
+ const sections = new Map();
162
+ const unsupportedSections = [];
163
+ const unsupportedVersions = [];
164
+ const warnings = [];
165
+ const knownIds = new Set(SECTION_IDS);
166
+ for (const [sectionId, enabled] of Object.entries(manifest.sections)) {
167
+ if (!enabled)
168
+ continue;
169
+ // G-01/G-02/G-03:manifest.sections 的键可能来自更新的 DSH(本版本不认识的分区)。
170
+ // 未知分区单独收集 → 由本函数产出独立告警;**绝不**再落进 missingSections
171
+ // (此前会被误报成「备份声明了但缺少的分区: X」——而文件其实就在 ZIP 里)。
172
+ if (!knownIds.has(sectionId)) {
173
+ unsupportedSections.push(sectionId);
174
+ continue;
175
+ }
176
+ const id = sectionId;
177
+ if (isFileSection(id)) {
178
+ const prefix = SECTION_FILE_PREFIXES[id];
179
+ const files = [];
180
+ for (const name of archive.names()) {
181
+ if (!name.startsWith(prefix) || name === prefix)
182
+ continue;
183
+ const rel = name.slice(prefix.length);
184
+ if (rel === '' || rel.endsWith('/'))
185
+ continue;
186
+ const data = archive.readEntry(name);
187
+ files.push({ relativePath: rel, data, contentHash: sha256Hex(data) });
188
+ }
189
+ sections.set(id, { version: 1, files });
190
+ continue;
191
+ }
192
+ const jsonPath = SECTION_JSON_PATHS[id];
193
+ if (jsonPath === undefined)
194
+ continue; // secrets 分区无 JSON 文件
195
+ if (!archive.has(jsonPath))
196
+ continue; // 声明包含但文件缺失 → 由调用方记 missingSections
197
+ let data;
198
+ try {
199
+ data = archive.readEntryJson(jsonPath);
200
+ }
201
+ catch (err) {
202
+ throw new Error(this.msg('import.sectionParseFailed', { section: id, reason: err instanceof Error ? err.message : String(err) }));
203
+ }
204
+ // G-05:分区数据 version 高于本版本支持的 1 → 跳过该分区(warning,不阻断整个 bundle)。
205
+ // 判定前置在此(不改 schema/config.ts 的校验语义):只有「数字且 > 1」才跳过;
206
+ // version < 1、非数字或缺失仍交由 validateSectionData 记硬错误(数据损坏语义不变)。
207
+ const rawVersion = (data !== null && typeof data === 'object')
208
+ ? data['version']
209
+ : undefined;
210
+ if (typeof rawVersion === 'number' && rawVersion > 1) {
211
+ unsupportedVersions.push({ section: id, version: rawVersion });
212
+ warnings.push(this.msg('import.unsupportedSectionVersion', { section: id, version: String(rawVersion) }));
213
+ continue;
214
+ }
215
+ const issues = validateSectionData(id, data);
216
+ const errors = issues.filter((i) => i.severity === 'error');
217
+ if (errors.length > 0) {
218
+ throw new Error(this.msg('import.sectionInvalid', { section: id, issues: errors.map((e) => e.message).join('; ') }));
219
+ }
220
+ sections.set(id, data);
221
+ }
222
+ // 未知分区汇总告警(放在版本告警之后:一条消息列出全部被跳过的未知分区)
223
+ if (unsupportedSections.length > 0) {
224
+ warnings.push(this.msg('import.unsupportedSections', { sections: unsupportedSections.join(', ') }));
225
+ }
226
+ return { sections, unsupportedSections, unsupportedVersions, warnings };
227
+ }
228
+ async analyzeBundle(bundle) {
229
+ const extraction = this.extractSections(bundle);
230
+ const { sections } = extraction;
231
+ const { manifest } = bundle;
232
+ const importCtx = {
233
+ manifest,
234
+ targetPlatform: this.ctx.platform,
235
+ target: this.ctx,
236
+ sections,
237
+ pathMappings: [],
238
+ resolutions: {},
239
+ secretInputs: {},
240
+ log: this.ctx.log,
241
+ msg: this.msg,
242
+ };
243
+ const adapterItems = [];
244
+ const adapterIssues = [];
245
+ for (const adapter of this.adapters) {
246
+ const data = sections.get(adapter.id);
247
+ if (data === undefined)
248
+ continue;
249
+ try {
250
+ const v = await adapter.validate(data, this.msg);
251
+ for (const issue of v.issues) {
252
+ if (issue.severity === 'error')
253
+ adapterIssues.push(this.msg('import.adapterValidationIssue', { adapter: adapter.id, message: issue.message }));
254
+ }
255
+ if (!v.valid)
256
+ continue;
257
+ const items = await adapter.analyzeImport(data, importCtx);
258
+ adapterItems.push(...items);
259
+ }
260
+ catch (err) {
261
+ adapterIssues.push(this.msg('import.adapterAnalyzeFailed', { adapter: adapter.id, reason: err instanceof Error ? err.message : String(err) }));
262
+ }
263
+ }
264
+ return {
265
+ ...bundle,
266
+ sections,
267
+ unsupportedSections: extraction.unsupportedSections,
268
+ unsupportedVersions: extraction.unsupportedVersions,
269
+ sectionWarnings: extraction.warnings,
270
+ adapterItems,
271
+ adapterIssues,
272
+ };
273
+ }
274
+ /* ---------------- 第 8 步:analyzeImport ---------------- */
275
+ async analyzeImport(zipPath) {
276
+ const bundle = await this.loadBundle(zipPath);
277
+ const { manifest, zipWarnings } = bundle;
278
+ const analyzed = await this.analyzeBundle(bundle);
279
+ const errors = [...analyzed.adapterIssues];
280
+ const warnings = [...zipWarnings, ...bundle.migrationWarnings, ...analyzed.sectionWarnings];
281
+ if (!canImport(manifest.schemaVersion)) {
282
+ errors.push(this.msg('import.versionUnsupported', { version: describeVersion(manifest.schemaVersion) }));
283
+ }
284
+ // 兼容性(第 6 步)
285
+ const sectionsInZip = [...analyzed.sections.keys()];
286
+ // G-01/G-02/G-03/G-05:被「跳过」的分区(本版本不认识的未知分区 / 数据版本过高)**不是缺失**,
287
+ // 必须从 missingSections 剔除;missingSections 只统计「已知分区但 ZIP 内文件缺失」。
288
+ const unsupportedSections = analyzed.unsupportedSections;
289
+ const skippedSections = new Set([
290
+ ...unsupportedSections,
291
+ ...analyzed.unsupportedVersions.map((u) => u.section),
292
+ ]);
293
+ const missingSections = Object.entries(manifest.sections)
294
+ .filter(([id, on]) => on && !skippedSections.has(id) && !analyzed.sections.has(id))
295
+ .map(([id]) => id);
296
+ if (missingSections.length > 0) {
297
+ warnings.push(this.msg('import.missingSections', { sections: missingSections.join(', ') }));
298
+ }
299
+ const compatibility = computeCompatibility({
300
+ sourceDsh: manifest.source.dshVersion,
301
+ targetDsh: this.ctx.dshVersion,
302
+ sourcePlatform: manifest.source.platform,
303
+ targetPlatform: this.ctx.platform,
304
+ schemaVersion: manifest.schemaVersion,
305
+ missingSections,
306
+ });
307
+ // 路径问题(第 12 步检测;核心只做形态判定,最终映射由 UI 确认)
308
+ const pathIssues = detectPathIssues(manifest.source.platform, this.ctx.platform, analyzed.sections);
309
+ // 依赖检测(§15:缺失不阻塞,标记 Requires Attention)
310
+ const dependencyIssues = [];
311
+ if (this.dependencyChecker) {
312
+ const mcp = analyzed.sections.get('mcp');
313
+ for (const server of mcp?.servers ?? []) {
314
+ const cmd = (server.command ?? '').trim();
315
+ if (cmd === '')
316
+ continue;
317
+ const base = cmd.split(/[\\/ ]/).pop() ?? cmd;
318
+ if (!KNOWN_DEPENDENCIES.has(base))
319
+ continue;
320
+ try {
321
+ const ok = await this.dependencyChecker(base);
322
+ if (!ok)
323
+ dependencyIssues.push({ item: server.serverName ?? base, dependency: base });
324
+ }
325
+ catch {
326
+ // 检查器异常视为未知,不误报
327
+ }
328
+ }
329
+ }
330
+ // 秘密计数:普通备份中已配置(有值)但未导出的凭据数
331
+ const creds = analyzed.sections.get('credentialsStatus');
332
+ const secretCount = (creds?.credentials ?? []).filter((c) => c.configured === true && c.hasValue !== true).length;
333
+ // 插件摘要
334
+ const plugins = analyzed.sections.get('plugins');
335
+ const toInstall = analyzed.adapterItems.filter((i) => i.kind === 'Install').length;
336
+ const pluginSummary = {
337
+ installed: Math.max((plugins?.plugins?.length ?? 0) - toInstall, 0),
338
+ toInstall,
339
+ };
340
+ return {
341
+ valid: errors.length === 0,
342
+ errors,
343
+ warnings,
344
+ compatibility,
345
+ sectionsInZip,
346
+ unsupportedSections,
347
+ unsupportedVersions: analyzed.unsupportedVersions,
348
+ pluginSummary,
349
+ pathIssues,
350
+ secretCount,
351
+ dependencyIssues,
352
+ encrypted: manifest.security.encrypted,
353
+ };
354
+ }
355
+ /* ---------------- 第 9 步:createImportPlan(Dry Run 复用) ---------------- */
356
+ async createImportPlan(zipPath, decisions) {
357
+ const bundle = await this.loadBundle(zipPath);
358
+ const analyzed = await this.analyzeBundle(bundle);
359
+ const { manifest } = bundle;
360
+ // 先把用户路径映射应用到各分区数据(PathMapper 先行:applyItem 拿到的已是映射后数据)
361
+ applyMappingsToSections(analyzed.sections, decisions.pathMappings);
362
+ const importCtx = {
363
+ manifest,
364
+ targetPlatform: this.ctx.platform,
365
+ target: this.ctx,
366
+ sections: analyzed.sections,
367
+ pathMappings: decisions.pathMappings,
368
+ resolutions: decisions.resolutions,
369
+ secretInputs: {},
370
+ log: this.ctx.log,
371
+ msg: this.msg,
372
+ };
373
+ const items = analyzed.adapterItems.map((item) => applyItemResolution(item, decisions, this.msg));
374
+ const planMappings = mergePathMappings(items, decisions.pathMappings);
375
+ // F4 删除墓碑过滤:读取本地删除墓碑(<dataDir>/tombstones.json;缺失/损坏 → 空列表安全降级),
376
+ // 剔除命中墓碑的计划项(插件 / 技能 / 文件类条目 + 分区级墓碑整分区跳过)。
377
+ // 过滤是纯函数(applyTombstoneFilter),此处只负责加载数据并调用。
378
+ const tombstones = await loadTombstones(this.ctx.fs, path.join(this.ctx.homeDir, 'dsh-config-manager'));
379
+ const { items: planItems, skipped: skippedTombstoned } = applyTombstoneFilter(items, tombstones);
380
+ // MissingSecret:兜底——credentialsStatus 分区里已配置的凭据若没有对应计划项,补占位
381
+ ensureMissingSecrets(planItems, analyzed.sections, this.msg);
382
+ const missingSecrets = planItems
383
+ .filter((i) => i.kind === 'MissingSecret')
384
+ .map((i) => ({ ref: i.id.replace(/^secret:/, ''), required: true }));
385
+ const needsRestart = planItems.some((i) => i.kind === 'Install' || (i.adapter === 'mcp' && i.kind !== 'Skip' && i.kind !== 'Warning'));
386
+ const estimatedActions = {};
387
+ for (const item of planItems) {
388
+ if (item.kind === 'Skip' || item.kind === 'Warning')
389
+ continue;
390
+ estimatedActions[item.adapter] = (estimatedActions[item.adapter] ?? 0) + 1;
391
+ }
392
+ return {
393
+ items: planItems,
394
+ globalStrategy: decisions.strategy,
395
+ pathMappings: planMappings,
396
+ missingSecrets,
397
+ needsRestart,
398
+ estimatedActions,
399
+ skippedTombstoned,
400
+ };
401
+ }
402
+ /* ---------------- 第 11-14 步:executeImportPlan(快照 → 执行 → 校验 → 结果/回滚) ---------------- */
403
+ async executeImportPlan(zipPath, plan, opts = {}) {
404
+ const bundle = await this.loadBundle(zipPath);
405
+ const analyzed = await this.analyzeBundle(bundle);
406
+ applyMappingsToSections(analyzed.sections, plan.pathMappings);
407
+ // 10. 用户确认(安全阀:不确认绝不动数据)
408
+ if (opts.confirm !== true) {
409
+ throw new ImportNotConfirmedError(this.msg);
410
+ }
411
+ // 10b. 加密不变量:加密备份必须已成功解密(decryptedCredentials 由宿主用备份密码
412
+ // 解开 security/secrets.enc 后注入)。未解密(undefined)一律拒绝执行——
413
+ // 不允许把加密凭据静默降级为「缺凭据照常导入」,否则加密备份与普通备份无区别。
414
+ if (bundle.manifest.security.encrypted && opts.decryptedCredentials === undefined) {
415
+ throw new Error(this.msg('import.encryptedPasswordRequired'));
416
+ }
417
+ const importCtx = {
418
+ manifest: bundle.manifest,
419
+ targetPlatform: this.ctx.platform,
420
+ target: this.ctx,
421
+ sections: analyzed.sections,
422
+ pathMappings: plan.pathMappings,
423
+ resolutions: {},
424
+ secretInputs: opts.secretInputs ?? {},
425
+ decryptedCredentials: opts.decryptedCredentials,
426
+ log: this.ctx.log,
427
+ msg: this.msg,
428
+ onLog: opts.onLog,
429
+ };
430
+ // 11. 快照(强制:导入前必须先备份将被修改的目标)。
431
+ // Phase 4:宿主注入 snapshotBinding 时,把 op-bound 元数据写入快照,并立即绑定 journal
432
+ // (SNAPSHOT_CREATED),保证「快照 durable+verified 且 journal 已知」先于首个 mutation。
433
+ const snapshot = await createSnapshot({
434
+ ctx: this.ctx,
435
+ plan,
436
+ sourceZip: zipPath,
437
+ store: this.snapshotStore,
438
+ adapters: this.adapters,
439
+ operationId: opts.snapshotBinding?.operationId,
440
+ operationType: opts.snapshotBinding?.operationType,
441
+ environmentFingerprint: opts.snapshotBinding?.environmentFingerprint,
442
+ ownerInstanceId: opts.snapshotBinding?.ownerInstanceId,
443
+ });
444
+ if (opts.snapshotBinding !== undefined) {
445
+ await opts.snapshotBinding.bindSnapshot(snapshot.id);
446
+ // 首个 destructive side effect 前:SNAPSHOT_CREATED→APPLYING(在首个 applyOne 之前调 markApplying)
447
+ await opts.snapshotBinding.markApplying();
448
+ }
449
+ // P2-B(Phase 8):逐计划项 WAL 预登记。将产生真实 side effect 的项在 apply 之前
450
+ // 记为 planned(文件类带 beforeFp),使 crash 中途可按「仍 planned ⇒ 未应用」保守判定;
451
+ // 非文件项 external=true(不可证明 → reconcile 恒 needs-attention,安全边界不放宽)。
452
+ const sb = opts.snapshotBinding;
453
+ const beforeFpByItem = new Map();
454
+ if (sb?.recordStep !== undefined) {
455
+ for (const item of plan.items) {
456
+ if (!this.shouldJournalStep(item))
457
+ continue;
458
+ let rel = null;
459
+ let beforeFp = null;
460
+ if (Analyzer.fileRelFor(item, importCtx.target.profile) !== null) {
461
+ rel = Analyzer.fileRelFor(item, importCtx.target.profile);
462
+ beforeFp = await this.fileFp(importCtx, rel);
463
+ }
464
+ beforeFpByItem.set(item.id, beforeFp);
465
+ await sb.recordStep({
466
+ id: item.id,
467
+ adapter: item.adapter,
468
+ kind: item.kind,
469
+ ref: rel !== null ? resolveFileTarget(importCtx.target, item.adapter, item.target?.ref ?? '') : '',
470
+ external: rel === null,
471
+ status: 'planned',
472
+ beforeFp,
473
+ afterFp: null,
474
+ });
475
+ }
476
+ }
477
+ const executed = [];
478
+ const warnings = [...bundle.zipWarnings, ...bundle.migrationWarnings, ...analyzed.sectionWarnings];
479
+ let needsRestart = plan.needsRestart;
480
+ let anyFailed = false;
481
+ // 12. 分阶段执行
482
+ const byAdapter = new Map();
483
+ for (const item of plan.items) {
484
+ const list = byAdapter.get(item.adapter) ?? [];
485
+ list.push(item);
486
+ byAdapter.set(item.adapter, list);
487
+ }
488
+ const totalItems = APPLY_ORDER.reduce((sum, id) => sum + (byAdapter.get(id)?.length ?? 0), 0);
489
+ let itemIndex = 0;
490
+ for (const adapterId of APPLY_ORDER) {
491
+ const adapter = this.adapters.find((a) => a.id === adapterId);
492
+ if (!adapter)
493
+ continue;
494
+ for (const item of byAdapter.get(adapterId) ?? []) {
495
+ // 每项一个 AbortController:宿主可 abort「当前项」(用户跳过当前插件)→
496
+ // 该项子进程被杀、标记为 user-skipped;不影响后续项执行。
497
+ const controller = new AbortController();
498
+ importCtx.signal = controller.signal;
499
+ // m1 埋点:每开始一个计划项上报(UI 显示「正在执行项 X」/ 判定跳过按钮)
500
+ const startIndex = itemIndex + 1;
501
+ try {
502
+ opts.onItemStart?.({ adapter: item.adapter, index: startIndex, total: totalItems, detail: item.id });
503
+ }
504
+ catch {
505
+ // 埋点回调失败不影响导入执行
506
+ }
507
+ const outcome = await this.applyOne(adapter, item, importCtx);
508
+ importCtx.signal = undefined;
509
+ // m1 埋点:每完成一个计划项上报(真实进度;onItem 抛错不得中断导入)
510
+ itemIndex += 1;
511
+ try {
512
+ opts.onItem?.({
513
+ adapter: item.adapter,
514
+ index: itemIndex,
515
+ total: totalItems,
516
+ status: outcome.executed.status,
517
+ detail: item.id,
518
+ });
519
+ }
520
+ catch {
521
+ // 埋点回调失败不影响导入执行(进度是尽力而为)
522
+ }
523
+ executed.push(outcome.executed);
524
+ // P2-B(Phase 8):逐项更新 journal step。文件类成功 → done + afterFp(reconcile 可判
525
+ // recovered);失败/warning/跳过 → 无不可靠 afterFp(attention/skipped → 保守 needs-attention)。
526
+ if (sb?.recordStep !== undefined && beforeFpByItem.has(item.id)) {
527
+ const rel = Analyzer.fileRelFor(item, importCtx.target.profile);
528
+ let afterFp = null;
529
+ if (rel !== null && outcome.executed.status === 'ok') {
530
+ afterFp = await this.fileFp(importCtx, rel);
531
+ }
532
+ await sb.recordStep({
533
+ id: item.id,
534
+ adapter: item.adapter,
535
+ kind: item.kind,
536
+ ref: rel !== null ? resolveFileTarget(importCtx.target, item.adapter, item.target?.ref ?? '') : '',
537
+ external: rel === null,
538
+ // issue #35:'warning'(如插件安装失败但非致命 §34.17)此前被记为 'skipped',
539
+ // 与「用户主动跳过」在持久层不可区分 —— 事后审计会得出「用户跳过了这些插件」的错误结论。
540
+ // 失败/警告都是**不可证明已应用** → 一律 attention;'skipped' 只留给真正的跳过。
541
+ status: outcome.executed.status === 'ok'
542
+ ? 'done'
543
+ : outcome.executed.status === 'skipped'
544
+ ? 'skipped'
545
+ : 'attention',
546
+ beforeFp: beforeFpByItem.get(item.id) ?? null,
547
+ afterFp,
548
+ message: outcome.executed.message ?? null,
549
+ });
550
+ }
551
+ // 仅硬失败计入 anyFailed(warning 属非致命:目标不可达等,不触发回滚,§34.17)
552
+ if (outcome.executed.status === 'failed')
553
+ anyFailed = true;
554
+ if (outcome.needsRestart)
555
+ needsRestart = true;
556
+ if (outcome.warning)
557
+ warnings.push(outcome.warning);
558
+ if (opts.rollbackOnError && outcome.executed.status === 'failed')
559
+ break;
560
+ }
561
+ if (opts.rollbackOnError && anyFailed)
562
+ break;
563
+ }
564
+ // 失败整体回滚(rollbackOnError):逆序补偿 + 诚实报告
565
+ if (opts.rollbackOnError && anyFailed) {
566
+ const rollbackReport = await rollback({
567
+ ctx: this.ctx,
568
+ snapshot,
569
+ store: this.snapshotStore,
570
+ adapters: this.adapters,
571
+ });
572
+ // M1:回滚完成 → 快照标记 rolled-back(元数据写失败只告警,不影响回滚结论)
573
+ await this.markSnapshotStatus(snapshot.id, 'rolled-back');
574
+ this.ctx.log.warn(`导入失败,已回滚(${rollbackReport.full ? '完整' : '部分'})`, {
575
+ failed: executed.filter((e) => e.status === 'failed').map((e) => e.itemId),
576
+ });
577
+ return {
578
+ ok: false,
579
+ executed,
580
+ needsRestart: false,
581
+ missingSecrets: [],
582
+ warnings,
583
+ rollback: rollbackReport,
584
+ snapshotId: snapshot.id,
585
+ skippedTombstoned: plan.skippedTombstoned ?? [],
586
+ };
587
+ }
588
+ // 13. 校验(执行后:对最终数据再 validate;失败仅告警,不掩盖已完成项)
589
+ for (const adapter of this.adapters) {
590
+ const data = analyzed.sections.get(adapter.id);
591
+ if (data === undefined)
592
+ continue;
593
+ try {
594
+ const v = await adapter.validate(data, this.msg);
595
+ for (const issue of v.issues) {
596
+ if (issue.severity === 'error')
597
+ warnings.push(this.msg('import.postValidationIssue', { adapter: adapter.id, message: issue.message }));
598
+ }
599
+ }
600
+ catch (err) {
601
+ warnings.push(this.msg('import.postValidationFailed', { adapter: adapter.id, reason: err instanceof Error ? err.message : String(err) }));
602
+ }
603
+ }
604
+ // 14. 结果
605
+ const missingSecrets = plan.missingSecrets
606
+ .filter((s) => !importCtx.decryptedCredentials?.has(s.ref) && !importCtx.secretInputs[s.ref])
607
+ .map((s) => s.ref);
608
+ // M1:导入成功 → 快照标记 done(元数据写失败只告警,不改变导入结论)
609
+ await this.markSnapshotStatus(snapshot.id, 'done');
610
+ // F1 vault 回填:导出时敏感文件(.credentials.yaml 等)明文未进备份(includeSecrets=false
611
+ // 时镜像到 <dataDir>/vault),导入成功后从本机 vault 回填 $DSH_HOME;vault 缺失
612
+ // (跨机恢复 / 从未镜像过)记入警告提示用户重填。尽力而为:失败仅警告,不影响导入结论。
613
+ try {
614
+ const vaultDataDir = path.join(this.ctx.homeDir, 'dsh-config-manager');
615
+ const vault = await restoreVaultFiles(this.ctx.fs, vaultDataDir, this.ctx.homeDir, DEFAULT_SENSITIVE_RELS);
616
+ for (const rel of vault.restored)
617
+ warnings.push(this.msg('import.vaultRestored', { rel }));
618
+ for (const rel of vault.missing)
619
+ warnings.push(this.msg('import.vaultMissing', { rel }));
620
+ for (const s of vault.skipped) {
621
+ // targetExists = 目标已有更新的凭据,属预期跳过,不打扰用户
622
+ if (s.reason !== 'targetExists')
623
+ warnings.push(this.msg('import.vaultBackfillFailed', { rel: s.rel, reason: s.reason }));
624
+ }
625
+ }
626
+ catch (err) {
627
+ warnings.push(this.msg('import.vaultBackfillFailed', { rel: DEFAULT_SENSITIVE_RELS.join(', '), reason: err instanceof Error ? err.message : String(err) }));
628
+ }
629
+ return {
630
+ ok: true, // 单项失败已如实记录在 executed;无未捕获异常即完成
631
+ executed,
632
+ needsRestart,
633
+ missingSecrets,
634
+ warnings,
635
+ rollback: null,
636
+ snapshotId: snapshot.id,
637
+ skippedTombstoned: plan.skippedTombstoned ?? [],
638
+ };
639
+ }
640
+ /** 快照状态标记(M1):成功→done / 失败回滚→rolled-back。元数据写失败只告警不抛错。 */
641
+ async markSnapshotStatus(id, status) {
642
+ try {
643
+ await this.snapshotStore.updateStatus(id, status);
644
+ }
645
+ catch (err) {
646
+ this.ctx.log.warn(`快照 ${id} 状态标记 ${status} 失败: ${err instanceof Error ? err.message : String(err)}`);
647
+ }
648
+ }
649
+ /* -------------------------------------------------- P2-B 逐计划项指纹(Phase 8) */
650
+ /**
651
+ * 该计划项是否会产生真实 side effect(需 journal step 追踪)。
652
+ * 排除无副作用的「信息/跳过/错误」项与未采用其导入内容的 Conflict ——
653
+ * 这些不写目标,记录只会保守化 reconcile 而无收益。
654
+ */
655
+ shouldJournalStep(item) {
656
+ if (item.kind === 'Skip' || item.kind === 'Warning' || item.kind === 'MissingDependency' || item.kind === 'Error')
657
+ return false;
658
+ if (item.kind === 'Conflict' && item.conflict?.resolution !== 'useImported')
659
+ return false;
660
+ return true;
661
+ }
662
+ /** 文件类且可指纹 → 返回 home-relative 目标路径(posix);否则 null(不可指纹外部项)。
663
+ * profile 必须传入:plugins 分区的 pnpm-workspace.yaml / patch 文件位于
664
+ * `profiles/<profile>/` 下,缺了它算出的相对路径不存在 → 指纹恒 null(issue #35)。 */
665
+ static fileRelFor(item, profile) {
666
+ if (!isFileSection(item.adapter))
667
+ return null;
668
+ const ref = item.target?.ref;
669
+ if (ref === undefined || ref === '')
670
+ return null;
671
+ return resolveFileTargetRel(item.adapter, ref, profile);
672
+ }
673
+ /** 读目标文件算 sha256(home-relative;经 HostContext.fs 使测试 mock 可注入)。失败返回 null。 */
674
+ async fileFp(ctx, rel) {
675
+ try {
676
+ const data = await ctx.target.fs.readFile(rel);
677
+ return sha256Hex(data);
678
+ }
679
+ catch {
680
+ return null;
681
+ }
682
+ }
683
+ async applyOne(adapter, item, ctx) {
684
+ // 执行日志(导入面板展示):仅非敏感文本(项 id / 命令),绝不写密钥/密码/补录值
685
+ const onLog = ctx.onLog;
686
+ if (item.kind === 'Skip' ||
687
+ item.kind === 'Warning' ||
688
+ item.kind === 'MissingDependency' ||
689
+ (item.kind === 'Conflict' && item.conflict?.resolution !== 'useImported')) {
690
+ // Warning / MissingDependency 是信息项(依赖缺失、需人工注意),不调用 applyItem。
691
+ onLog?.(`– ${item.id}`);
692
+ return { executed: { itemId: item.id, status: 'skipped' }, needsRestart: false };
693
+ }
694
+ if (item.kind === 'Error') {
695
+ onLog?.(`✗ ${item.id}`);
696
+ return { executed: { itemId: item.id, status: 'failed', message: item.detail ?? item.description }, needsRestart: false };
697
+ }
698
+ if (item.kind === 'MissingSecret') {
699
+ const ref = item.id.replace(/^secret:/, '');
700
+ const value = ctx.decryptedCredentials?.get(ref) ?? ctx.secretInputs[ref];
701
+ if (value === undefined || value === '') {
702
+ onLog?.(`– ${item.id}`);
703
+ return { executed: { itemId: item.id, status: 'skipped', message: this.msg('import.secretNotProvided') }, needsRestart: false };
704
+ }
705
+ // 补录值只经 adapter.applyItem 写入(m5 实现),引擎不直接触碰凭据
706
+ }
707
+ onLog?.(`▶ ${item.id}`);
708
+ try {
709
+ const result = await adapter.applyItem(item, ctx);
710
+ const status = result.ok ? 'ok' : (result.warning === true ? 'warning' : 'failed');
711
+ onLog?.(`${status === 'ok' ? '✓' : status === 'warning' ? '⚠' : '✗'} ${item.id}`);
712
+ return {
713
+ executed: { itemId: item.id, status, message: result.message },
714
+ needsRestart: result.needsRestart === true,
715
+ warning: result.warning === true ? `${item.id}: ${result.message ?? item.description}` : undefined,
716
+ };
717
+ }
718
+ catch (err) {
719
+ // 用户跳过(宿主 install 中止子进程):记为 skipped + skippedByUser,非失败,不触发回滚
720
+ if (err instanceof ImportUserSkippedError) {
721
+ onLog?.(`⏭ ${item.id}`);
722
+ return {
723
+ executed: {
724
+ itemId: item.id,
725
+ status: 'skipped',
726
+ skippedByUser: true,
727
+ message: this.msg('import.userSkipped'),
728
+ },
729
+ needsRestart: false,
730
+ };
731
+ }
732
+ this.ctx.log.error(`应用计划项失败 ${item.id}: ${err instanceof Error ? err.message : String(err)}`);
733
+ onLog?.(`✗ ${item.id}`);
734
+ return {
735
+ executed: { itemId: item.id, status: 'failed', message: err instanceof Error ? err.message : String(err) },
736
+ needsRestart: false,
737
+ };
738
+ }
739
+ }
740
+ }
741
+ /* ---------------- 纯函数辅助 ---------------- */
742
+ /**
743
+ * G-06:执行 schema 迁移链并把迁移结果重新校验为合法 manifest。
744
+ *
745
+ * `migrateToCurrent` 此前在 `src/` 内零引用(迁移链有定义、有单测、从不执行);
746
+ * `loadBundle` 只用 isSupported 做判定就直接把旧文档当新格式使用。本函数是迁移链的
747
+ * **真实接线点**:沿链迁移 → 重新校验结果是合法 manifest(不合法则抛明确错误,
748
+ * 绝不把半迁移文档当合法 manifest 继续)→ 每个已应用步骤翻译成用户可见告警。
749
+ *
750
+ * 显式传入 from/target 使「链式迁移真的被执行」可被直接单测:当前
751
+ * `MIN_SUPPORTED_SCHEMA_VERSION = CURRENT_SCHEMA_VERSION = 1`,`needsMigration` 恒假,
752
+ * 因此该分支在真实导入中结构上不可达(loadBundle 仍按 needsMigration 调用本函数)。
753
+ */
754
+ export function runSchemaMigration(doc, fromVersion, targetVersion, msg) {
755
+ const result = migrateToCurrent(doc, fromVersion, targetVersion);
756
+ const issues = validateManifest(result.doc).filter((i) => i.severity === 'error');
757
+ if (issues.length > 0) {
758
+ throw new Error(msg('import.migrateInvalidManifest', { issues: issues.map((i) => i.message).join('; ') }));
759
+ }
760
+ return {
761
+ manifest: result.doc,
762
+ warnings: result.applied.map((step) => msg('import.migrated', { from: String(step.from), to: String(step.to) })),
763
+ };
764
+ }
765
+ async function verifyAgainstTable(entries, table) {
766
+ const mismatches = [];
767
+ const missing = [];
768
+ for (const [relPath, expected] of Object.entries(table)) {
769
+ const data = entries.get(relPath);
770
+ if (data === undefined) {
771
+ missing.push(relPath);
772
+ continue;
773
+ }
774
+ if (sha256Hex(data) !== expected)
775
+ mismatches.push(relPath);
776
+ }
777
+ return { ok: mismatches.length === 0 && missing.length === 0, mismatches, missing };
778
+ }
779
+ /** 应用用户冲突决策 + 全局策略(纯函数,返回新数组) */
780
+ function applyItemResolution(item, decisions, msg) {
781
+ if (item.kind !== 'Conflict')
782
+ return item;
783
+ const resolution = decisions.resolutions[item.id];
784
+ if (resolution === 'keepCurrent') {
785
+ return { ...item, kind: 'Skip', severity: 'info', detail: `${item.detail ?? ''}${msg('import.conflictKeepCurrent')}` };
786
+ }
787
+ if (resolution === 'useImported') {
788
+ return { ...item, kind: 'Update', severity: 'info', conflict: { itemId: item.id, resolution } };
789
+ }
790
+ // review / 未决策:按全局策略兜底
791
+ if (decisions.strategy === 'skipExisting') {
792
+ return { ...item, kind: 'Skip', severity: 'info', detail: `${item.detail ?? ''}${msg('import.conflictSkipExisting')}` };
793
+ }
794
+ if (decisions.strategy === 'replace') {
795
+ return { ...item, kind: 'Update', severity: 'info', detail: `${item.detail ?? ''}${msg('import.conflictReplace')}` };
796
+ }
797
+ return item; // merge + 未决策 → 保持 Conflict,由报告列明
798
+ }
799
+ /** 路径映射合并:只保留「已解析」(newPrefix 非空)的映射用于执行期数据改写;
800
+ * 未解决项(newPrefix 空)保留在 plan.items 里供 UI 提示,但不参与数据映射。 */
801
+ function mergePathMappings(items, userMappings) {
802
+ const merged = new Map();
803
+ for (const m of userMappings) {
804
+ if (m.newPrefix !== '')
805
+ merged.set(m.oldPrefix, m);
806
+ }
807
+ return [...merged.values()];
808
+ }
809
+ /** 把映射应用到分区数据(PathMapper 先行:只替换匹配前缀的字符串叶值) */
810
+ function applyMappingsToSections(sections, mappings) {
811
+ if (mappings.length === 0)
812
+ return;
813
+ for (const [sectionId, data] of sections) {
814
+ const appliesTo = sectionId === 'workspaces' ? ['workspaces']
815
+ : sectionId === 'mcp' ? ['mcp']
816
+ : sectionId === 'skills' ? ['skills']
817
+ : sectionId === 'pluginFiles' ? ['pluginConfig']
818
+ : [];
819
+ const relevant = mappings.filter((m) => m.appliesTo.some((a) => appliesTo.includes(a)) || m.appliesTo.length === 0);
820
+ if (relevant.length === 0)
821
+ continue;
822
+ sections.set(sectionId, applyPrefixMappings(data, relevant));
823
+ }
824
+ }
825
+ /** 路径形态判定:跨平台盘符/UNIX 冲突 → platformMismatch;其余绝对路径 → missing(需映射) */
826
+ function detectPathIssues(sourcePlatform, targetPlatform, sections) {
827
+ const issues = [];
828
+ const workspaces = sections.get('workspaces');
829
+ for (const w of workspaces?.workspaces ?? []) {
830
+ const p = w.path;
831
+ if (p && isAbsolutePath(p))
832
+ issues.push(judgePath(p, sourcePlatform, targetPlatform));
833
+ }
834
+ const mcp = sections.get('mcp');
835
+ for (const s of mcp?.servers ?? []) {
836
+ for (const p of [s.cwd, s.command]) {
837
+ if (p && isAbsolutePath(p))
838
+ issues.push(judgePath(p, sourcePlatform, targetPlatform));
839
+ }
840
+ }
841
+ // 去重
842
+ const seen = new Set();
843
+ return issues.filter((i) => {
844
+ const key = `${i.kind}:${i.value}`;
845
+ if (seen.has(key))
846
+ return false;
847
+ seen.add(key);
848
+ return true;
849
+ });
850
+ }
851
+ function judgePath(p, sourcePlatform, targetPlatform) {
852
+ const isWinStyle = /^[a-zA-Z]:[\\/]/.test(p);
853
+ const isUnixStyle = p.startsWith('/');
854
+ if (sourcePlatform !== targetPlatform && ((isWinStyle && targetPlatform !== 'win32') || (isUnixStyle && targetPlatform === 'win32'))) {
855
+ return { kind: 'platformMismatch', value: p };
856
+ }
857
+ return { kind: 'missing', value: p };
858
+ }
859
+ /** MissingSecret 兜底:credentialsStatus 分区已配置凭据若无对应计划项,补占位 */
860
+ function ensureMissingSecrets(items, sections, msg) {
861
+ const creds = sections.get('credentialsStatus');
862
+ if (!creds?.credentials)
863
+ return;
864
+ const existing = new Set(items.filter((i) => i.kind === 'MissingSecret').map((i) => i.id));
865
+ for (const c of creds.credentials) {
866
+ if (!c.ref || c.configured !== true)
867
+ continue;
868
+ const id = `secret:${c.ref}`;
869
+ if (existing.has(id))
870
+ continue;
871
+ items.push({
872
+ id,
873
+ kind: 'MissingSecret',
874
+ adapter: 'credentialsStatus',
875
+ description: msg('import.secretMissingDesc', { ref: c.ref }),
876
+ severity: 'warning',
877
+ target: { adapter: 'credentialsStatus', ref: c.ref },
878
+ });
879
+ existing.add(id);
880
+ }
881
+ }
882
+ /* ---------------- F4 删除墓碑过滤(纯函数,可独立测试) ---------------- */
883
+ /**
884
+ * 按删除墓碑过滤计划项(F4):
885
+ * - 条目级:插件(Install/Update/Conflict)、技能(skills 文件)、文件类条目(agentPresets 等)命中墓碑 → 剔除;
886
+ * - 分区级:墓碑 kind='section' 且 id=分区 id → 整分区跳过(仅记一条 skipped)。
887
+ * 返回过滤后的 items 与被跳过清单(纯函数:不读文件、不改入参)。
888
+ */
889
+ function applyTombstoneFilter(items, tombstones) {
890
+ if (tombstones.length === 0)
891
+ return { items, skipped: [] };
892
+ const tombstonedSections = new Set(tombstones.filter((t) => t.kind === 'section').map((t) => t.id));
893
+ const skipped = [];
894
+ const out = [];
895
+ const reportedSections = new Set();
896
+ for (const item of items) {
897
+ // 分区级墓碑优先:整个分区不出现(含 Skip 等非写入项,UI 提示「该分区已删除」)
898
+ if (tombstonedSections.has(item.adapter)) {
899
+ if (!reportedSections.has(item.adapter)) {
900
+ skipped.push({ kind: 'section', id: item.adapter, adapter: item.adapter });
901
+ reportedSections.add(item.adapter);
902
+ }
903
+ continue;
904
+ }
905
+ const key = tombstoneKeyForItem(item);
906
+ if (key !== null && isTombstoned(key.kind, key.id, tombstones)) {
907
+ skipped.push({ kind: key.kind, id: key.id, adapter: item.adapter });
908
+ continue;
909
+ }
910
+ out.push(item);
911
+ }
912
+ return { items: out, skipped };
913
+ }
914
+ /** PlanItem → 墓碑键({kind,id})映射;非「将写入」的条目(Skip 等)与配置类项返回 null 不过滤 */
915
+ function tombstoneKeyForItem(item) {
916
+ // 插件本体(plugin:<pkg>):Install / Update / Conflict 均视为「将写入该插件」→ 命中墓碑剔除;
917
+ // patch: 行与 plugins:pnpm-workspace 是配置类项,不按插件墓碑过滤(分区级墓碑可覆盖)。
918
+ if (item.adapter === 'plugins' && item.id.startsWith('plugin:')) {
919
+ if (item.kind !== 'Install' && item.kind !== 'Update' && item.kind !== 'Conflict')
920
+ return null;
921
+ return { kind: 'plugin', id: item.id.slice('plugin:'.length) };
922
+ }
923
+ // 文件类分区条目(id = <分区>:<相对路径>):skills → skill 墓碑;其余文件类(agentPresets/
924
+ // agentInstructions/pluginFiles/sessions/self)→ 通用 file 墓碑(id 为相对路径)。
925
+ if (item.kind === 'Create' || item.kind === 'Update' || item.kind === 'Conflict') {
926
+ const prefix = `${item.adapter}:`;
927
+ if (item.adapter === 'skills')
928
+ return { kind: 'skill', id: item.id.slice(prefix.length) };
929
+ if (isFileSection(item.adapter))
930
+ return { kind: 'file', id: item.id.slice(prefix.length) };
931
+ }
932
+ return null;
933
+ }
934
+ //# sourceMappingURL=analyzer.js.map