@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,143 @@
1
+ /**
2
+ * credentialsStatus 分区 adapter(设计 §3.3/§7.1):
3
+ * 数据源 = ctx.credentials.describe(ref) 的状态(configured/source/writable),
4
+ * 以及 settings secrets 标记 / llm apiKeyEnv 中引用的凭据 ref 名。
5
+ *
6
+ * 安全不变量:永不导出值(hasValue 恒 false);导入生成 MissingSecret 清单,
7
+ * 用户补录值经 ctx.secretInputs / decryptedCredentials(仅内存)→ credentials.set()。
8
+ * .credentials.yaml 文件字节交由 m4 加密层处理,本 adapter 不触碰。
9
+ */
10
+ import type { CredentialStatus, CredentialsSection } from '../schema/types.ts';
11
+ import { msgOf, zhMsg } from '../core/messages.ts';
12
+ import type { MsgFunc } from '../core/messages.ts';
13
+ import type {
14
+ ApplyResult, ConfigAdapter, ExportOptions, ExportSection, HostContext,
15
+ ImportContext, PlanItem, ValidationResult,
16
+ } from '../core/types.ts';
17
+ import { resolveNamespaces, type NamespaceProvider } from './settings.ts';
18
+
19
+ export type CredentialRefsProvider = (ctx: HostContext) => Promise<string[]>;
20
+
21
+ /** 缺省 ref 收集:遍历 settings namespace,收集 llm apiKeyEnv / providers[].apiKeyEnv,
22
+ * 以及 secrets 标记中「引用类字段」(apiKeyEnv/tokenEnv…)的字段值。
23
+ * 注意 secrets[].path[0] 是 settings 文档内的字段路径(如 ['apiKey']),本身不是凭据 ref(设计 §4.2),
24
+ * 只有指向 env 名的引用字段才值得收集。 */
25
+ export function defaultCredentialRefs(namespaces: string[] | NamespaceProvider): CredentialRefsProvider {
26
+ return async (ctx: HostContext): Promise<string[]> => {
27
+ const refs = new Set<string>();
28
+ for (const ns of await resolveNamespaces(namespaces, ctx)) {
29
+ try {
30
+ const info = await ctx.settings.describe(ns, { redactSecrets: true });
31
+ const value = (info.value ?? {}) as Record<string, unknown>;
32
+ // 引用类字段名(值 = env/凭据引用名,非秘密本身)
33
+ const REFERENCE_REF_FIELDS = new Set([
34
+ 'apikeyenv', 'api_key_env', 'apikeyname', 'tokenenv', 'accesstokenenv',
35
+ 'refreshtokenenv', 'clientsecretenv', 'passwordenv',
36
+ ]);
37
+ for (const s of info.secrets) {
38
+ const first = s.path?.[0];
39
+ if (typeof first !== 'string' || first === '') continue;
40
+ const norm = first.toLowerCase().replace(/[^a-z0-9]/g, '');
41
+ if (!REFERENCE_REF_FIELDS.has(norm)) continue;
42
+ const val = value[first];
43
+ if (typeof val === 'string' && val !== '') refs.add(val);
44
+ }
45
+ if (typeof value['apiKeyEnv'] === 'string' && value['apiKeyEnv'] !== '') refs.add(value['apiKeyEnv'] as string);
46
+ const providers = value['providers'];
47
+ if (providers !== null && typeof providers === 'object') {
48
+ for (const pv of Object.values(providers as Record<string, { apiKeyEnv?: unknown }>)) {
49
+ if (pv !== null && typeof pv === 'object' && typeof pv.apiKeyEnv === 'string' && pv.apiKeyEnv !== '') {
50
+ refs.add(pv.apiKeyEnv);
51
+ }
52
+ }
53
+ }
54
+ } catch {
55
+ // namespace 不存在则跳过
56
+ }
57
+ }
58
+ return [...refs];
59
+ };
60
+ }
61
+
62
+ export interface CredentialsAdapterOptions {
63
+ /** 凭据 ref 名收集器(缺省从 settings 推断) */
64
+ refs?: CredentialRefsProvider;
65
+ /** 供缺省 refs 使用的 namespace 清单 */
66
+ namespaces?: string[] | NamespaceProvider;
67
+ }
68
+
69
+ export class CredentialsAdapter implements ConfigAdapter<CredentialsSection> {
70
+ readonly id = 'credentialsStatus' as const;
71
+ readonly displayName = 'Credentials';
72
+ readonly defaultIncluded = true;
73
+ readonly portability = 'deviceSpecific' as const;
74
+ private readonly refs: CredentialRefsProvider;
75
+
76
+ constructor(options: CredentialsAdapterOptions = {}) {
77
+ this.refs = options.refs ?? defaultCredentialRefs(options.namespaces ?? []);
78
+ }
79
+
80
+ async export(ctx: HostContext, _options: ExportOptions): Promise<ExportSection<CredentialsSection>> {
81
+ const credentials: CredentialStatus[] = [];
82
+ const warnings: string[] = [];
83
+ for (const ref of await this.refs(ctx)) {
84
+ try {
85
+ const status = await ctx.credentials.describe(ref);
86
+ credentials.push({
87
+ ref,
88
+ required: true,
89
+ configured: status.configured,
90
+ source: (status.source as CredentialStatus['source']) ?? 'file',
91
+ hasValue: false, // 值未导出(安全不变量)
92
+ });
93
+ } catch (err) {
94
+ warnings.push(msgOf(ctx)('adapter.credStatusReadFailed', { ref, reason: err instanceof Error ? err.message : String(err) }));
95
+ }
96
+ }
97
+ return {
98
+ sectionId: 'credentialsStatus',
99
+ data: { version: 1, credentials },
100
+ counts: { credentials: credentials.length },
101
+ warnings,
102
+ };
103
+ }
104
+
105
+ async analyzeImport(_data: CredentialsSection, _ctx: ImportContext): Promise<PlanItem[]> {
106
+ // MissingSecret 计划项由引擎在 createImportPlan 兜底生成(analyzer.ensureMissingSecrets),
107
+ // 依据 = 本分区里 configured=true 的凭据 → 用户补录清单。这里保持零写入纯计算。
108
+ return [];
109
+ }
110
+
111
+ async applyItem(item: PlanItem, ctx: ImportContext): Promise<ApplyResult> {
112
+ const ref = item.target?.ref;
113
+ if (!ref) return { ok: false, message: ctx.msg('adapter.missingTargetRef') };
114
+ const value = ctx.secretInputs[ref] ?? ctx.decryptedCredentials?.get(ref);
115
+ if (value === undefined || value === '') return { ok: false, message: ctx.msg('adapter.credentialValueMissing') };
116
+ await ctx.target.credentials.set(ref, value);
117
+ return { ok: true };
118
+ }
119
+
120
+ async validate(data: CredentialsSection, msg: MsgFunc = zhMsg): Promise<ValidationResult> {
121
+ const issues: ValidationResult['issues'] = [];
122
+ if (data === null || typeof data !== 'object') {
123
+ return { valid: false, issues: [{ path: '$', message: msg('adapter.validate.object', { subject: 'credentials' }), severity: 'error' }] };
124
+ }
125
+ if (data.version !== 1) {
126
+ issues.push({ path: 'version', message: msg('adapter.validate.version', { value: String(data.version) }), severity: 'error' });
127
+ }
128
+ if (!Array.isArray(data.credentials)) {
129
+ issues.push({ path: 'credentials', message: msg('adapter.validate.array', { subject: 'credentials' }), severity: 'error' });
130
+ } else {
131
+ for (const c of data.credentials) {
132
+ if (c === null || typeof c !== 'object' || typeof c.ref !== 'string' || c.ref === '') {
133
+ issues.push({ path: 'credentials[]', message: msg('adapter.validate.credentialRef'), severity: 'error' });
134
+ }
135
+ if (c.hasValue === true) {
136
+ // 安全不变量:普通导出恒不携带值;若备份声称有值,视为结构异常
137
+ issues.push({ path: `credentials.${c.ref}.hasValue`, message: msg('adapter.validate.hasValueFalse'), severity: 'error' });
138
+ }
139
+ }
140
+ }
141
+ return { valid: issues.filter((i) => i.severity === 'error').length === 0, issues };
142
+ }
143
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * 文件类分区共享 adapter 基类(skills / agentPresets / sessions 复用;
3
+ * agentInstructions 因位于 homeDir 根、只能白名单收集,覆写 export 后复用本类其余逻辑)。
4
+ * 基准目录与 core/backup.ts 的 FILE_BASES 保持一致(skills→'skills'、agentPresets→'.agent-presets'、sessions→'sessions'),
5
+ * 保证引擎通用快照/回滚(resolveFileTarget)与 applyItem 的写入路径完全一致。
6
+ *
7
+ * 文件内容以真实文件进入 ZIP(custom/skills/… 等前缀由 exporter 按 SECTION_FILE_PREFIXES 处理)。
8
+ * 幂等:相对路径 + 内容 SHA-256 hash 比对(重复导入 → Skip)。
9
+ */
10
+ import path from 'node:path';
11
+ import { sha256Hex } from '../utils/hashing.ts';
12
+ import { isReservedInternalRel, normalizePath } from '../utils/paths.ts';
13
+ import { msgOf, zhMsg } from '../core/messages.ts';
14
+ import type { MsgFunc } from '../core/messages.ts';
15
+ import { linkWarnings, listFilesDetailed } from './link-report.ts';
16
+ import type { RecursiveListing } from '../utils/recursive-walk.ts';
17
+ import type { FilesSection, SectionId } from '../schema/types.ts';
18
+ import type {
19
+ ApplyResult, ConfigAdapter, ExportOptions, ExportSection, HostContext,
20
+ ImportContext, PlanItem, Portability, ValidationResult,
21
+ } from '../core/types.ts';
22
+
23
+ export abstract class FileCollectionAdapter implements ConfigAdapter<FilesSection> {
24
+ abstract readonly id: SectionId;
25
+ abstract readonly displayName: string;
26
+ abstract readonly defaultIncluded: boolean;
27
+ abstract readonly portability: Portability;
28
+ /** 相对 homeDir 的基准目录(与 core/backup.ts FILE_BASES 一致) */
29
+ abstract readonly baseDir: string;
30
+
31
+ async export(ctx: HostContext, _options: ExportOptions): Promise<ExportSection<FilesSection>> {
32
+ const files: FilesSection['files'] = [];
33
+ const warnings: string[] = [];
34
+ // issue #37:用「跟随 junction/符号链接」的遍历,并把跟随/跳过的链接写进告警——
35
+ // 此前链接目录及其全部内容被静默排除,备份仍报成功。
36
+ let listing: RecursiveListing = { paths: [], skippedLinks: [], followedLinks: 0, unreadableDirs: [] };
37
+ try {
38
+ listing = await listFilesDetailed(ctx.fs, this.baseDir);
39
+ } catch {
40
+ // 目录不存在视为空
41
+ }
42
+ const rels = listing.paths;
43
+ for (const rel of rels) {
44
+ const data = await ctx.fs.readFile(rel);
45
+ const relPath = this.baseDir === '' ? rel : rel.replace(new RegExp(`^${escapeRegExp(this.baseDir)}[\\/]`), '');
46
+ files.push({ relativePath: relPath, data, contentHash: sha256Hex(data) });
47
+ }
48
+ if (rels.length === 0) warnings.push(msgOf(ctx)('adapter.dirEmpty', { type: this.displayName }));
49
+ warnings.push(...linkWarnings(msgOf(ctx), this.displayName, listing));
50
+ return {
51
+ sectionId: this.id,
52
+ data: { version: 1, files },
53
+ counts: { files: files.length },
54
+ warnings,
55
+ };
56
+ }
57
+
58
+ async analyzeImport(data: FilesSection, ctx: ImportContext): Promise<PlanItem[]> {
59
+ const msg = ctx.msg;
60
+ const items: PlanItem[] = [];
61
+ for (const file of data.files) {
62
+ const id = `${this.id}:${file.relativePath}`;
63
+ // F23 修复:不可信 import 不得写内部 control-plane namespace。
64
+ // 检查 baseDir+ref 解析后的 homeDir 相对路径(self 适配器 baseDir='dsh-config-manager' 是主投毒向量)。
65
+ const resolvedRel = normalizePath(path.join(this.baseDir, file.relativePath));
66
+ if (isReservedInternalRel(resolvedRel)) {
67
+ items.push({
68
+ id, kind: 'Error', adapter: this.id,
69
+ description: msg('adapter.fileReserved', { path: file.relativePath }), severity: 'error',
70
+ });
71
+ continue;
72
+ }
73
+ let current: Uint8Array | null = null;
74
+ try {
75
+ current = await ctx.target.fs.readFile(path.join(this.baseDir, file.relativePath));
76
+ } catch {
77
+ current = null;
78
+ }
79
+ if (current === null) {
80
+ items.push({
81
+ id, kind: 'Create', adapter: this.id,
82
+ description: msg('adapter.fileCreate', { type: this.displayName, path: file.relativePath }), severity: 'info',
83
+ target: { adapter: this.id, ref: file.relativePath },
84
+ });
85
+ } else if (sha256Hex(current) === file.contentHash) {
86
+ items.push({ id, kind: 'Skip', adapter: this.id, description: msg('adapter.fileSame', { path: file.relativePath }), severity: 'info' });
87
+ } else {
88
+ items.push({
89
+ id, kind: 'Conflict', adapter: this.id,
90
+ description: msg('adapter.fileDiff', { path: file.relativePath }), severity: 'warning',
91
+ target: { adapter: this.id, ref: file.relativePath },
92
+ });
93
+ }
94
+ }
95
+ return items;
96
+ }
97
+
98
+ async applyItem(item: PlanItem, ctx: ImportContext): Promise<ApplyResult> {
99
+ const msg = ctx.msg;
100
+ const ref = item.target?.ref;
101
+ if (!ref) return { ok: false, message: msg('adapter.missingTargetRef') };
102
+ // F23 修复:apply 前拒绝写内部 control-plane namespace(纵深防御,analyzeImport 已标 Error)
103
+ const resolvedRel = normalizePath(path.join(this.baseDir, ref));
104
+ if (isReservedInternalRel(resolvedRel)) {
105
+ return { ok: false, message: msg('adapter.fileReserved', { path: ref }) };
106
+ }
107
+ const data = ctx.sections.get(this.id) as FilesSection | undefined;
108
+ const file = data?.files.find((f) => f.relativePath === ref);
109
+ if (!file) return { ok: false, message: msg('adapter.dataMissingFile', { ref }) };
110
+ await ctx.target.fs.writeFile(path.join(this.baseDir, ref), file.data);
111
+ return { ok: true };
112
+ }
113
+
114
+ async validate(data: FilesSection, msg: MsgFunc = zhMsg): Promise<ValidationResult> {
115
+ const issues: ValidationResult['issues'] = [];
116
+ if (data === null || typeof data !== 'object') {
117
+ return { valid: false, issues: [{ path: '$', message: msg('adapter.validate.fileSection'), severity: 'error' }] };
118
+ }
119
+ if (data.version !== 1) {
120
+ issues.push({ path: 'version', message: msg('adapter.validate.version', { value: String(data.version) }), severity: 'error' });
121
+ }
122
+ if (!Array.isArray(data.files)) {
123
+ issues.push({ path: 'files', message: msg('adapter.validate.array', { subject: 'files' }), severity: 'error' });
124
+ } else {
125
+ for (const f of data.files) {
126
+ if (f === null || typeof f !== 'object' || typeof f.relativePath !== 'string' || f.relativePath === '') {
127
+ issues.push({ path: 'files[]', message: msg('adapter.validate.fileRelativePath'), severity: 'error' });
128
+ }
129
+ }
130
+ }
131
+ return { valid: issues.filter((i) => i.severity === 'error').length === 0, issues };
132
+ }
133
+ }
134
+
135
+ function escapeRegExp(s: string): string {
136
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
137
+ }
@@ -0,0 +1,353 @@
1
+ /**
2
+ * 文件类分区 adapter 测试(skills / agentPresets / agentInstructions / pluginFiles / sessions):
3
+ * 导出收集文件、Create/Skip 分析、applyItem 写入、白名单与默认关闭语义。
4
+ */
5
+ import test from 'node:test';
6
+ import assert from 'node:assert/strict';
7
+ import { SkillsAdapter } from './skills.ts';
8
+ import { AgentPresetsAdapter } from './agent-presets.ts';
9
+ import { AgentInstructionsAdapter } from './agent-instructions.ts';
10
+ import { PluginFilesAdapter } from './plugin-files.ts';
11
+ import { SessionsAdapter } from './sessions.ts';
12
+ import { SelfAdapter } from './self.ts';
13
+ import { makeContext, makeImportContext, sha256Hex } from './test-helpers.ts';
14
+ import type { PlanItem } from '../core/types.ts';
15
+
16
+ test('skills: 导出收集文件 + 导入往返(hash 幂等)', async () => {
17
+ const src = makeContext('win32', 'C:\\Users\\alice');
18
+ await src.fs.writeFile('skills/coding.md', Buffer.from('# Coding skill\n', 'utf8'));
19
+ await src.fs.writeFile('skills/git.md', Buffer.from('# Git skill\n', 'utf8'));
20
+
21
+ const adapter = new SkillsAdapter();
22
+ const out = await adapter.export(src, { includeSecrets: false });
23
+ assert.equal(out.data.files.length, 2);
24
+ assert.equal(out.data.files[0]?.relativePath, 'coding.md');
25
+ assert.equal(out.data.files[0]?.contentHash, sha256Hex(Buffer.from('# Coding skill\n')));
26
+ assert.equal(out.counts.files, 2);
27
+
28
+ const sections = new Map([['skills', out.data]]);
29
+ const dst = makeContext('linux', '/home/bob');
30
+ let items = await adapter.analyzeImport(out.data, makeImportContext(dst, sections));
31
+ assert.equal(items.length, 2);
32
+ assert.ok(items.every((i) => i.kind === 'Create'));
33
+ for (const item of items) {
34
+ const r = await adapter.applyItem(item, makeImportContext(dst, sections));
35
+ assert.equal(r.ok, true);
36
+ }
37
+ assert.equal(Buffer.from(await dst.fs.readFile('skills/coding.md')).toString(), '# Coding skill\n');
38
+
39
+ // 幂等:一致 → Skip
40
+ items = await adapter.analyzeImport(out.data, makeImportContext(dst, sections));
41
+ assert.ok(items.every((i) => i.kind === 'Skip'));
42
+
43
+ // 内容不同 → Conflict
44
+ await dst.fs.writeFile('skills/coding.md', Buffer.from('# Changed\n', 'utf8'));
45
+ items = await adapter.analyzeImport(out.data, makeImportContext(dst, sections));
46
+ assert.ok(items.some((i) => i.kind === 'Conflict'));
47
+ });
48
+
49
+ test('issue #37: skills 导出把「跟随/跳过的链接」写进 warnings(不再静默缺失)', async () => {
50
+ const src = makeContext('win32', 'C:\\Users\\alice');
51
+ await src.fs.writeFile('skills/coding.md', Buffer.from('# Coding\n', 'utf8'));
52
+ // 链接目录下的真实内容(链接已跟随 → 内容确实可读)
53
+ await src.fs.writeFile('skills/shared/inner.md', Buffer.from('# Shared\n', 'utf8'));
54
+ // 宿主实现 listRecursiveDetailed(真实 FileSystemFacade 的形态):链接内容已收集,且带诊断
55
+ src.fs.listRecursiveDetailed = async () => ({
56
+ paths: ['skills/coding.md', 'skills/shared/inner.md'],
57
+ skippedLinks: [{ path: 'skills/broken', reason: 'broken' }],
58
+ followedLinks: 1,
59
+ unreadableDirs: [],
60
+ });
61
+ const out = await new SkillsAdapter().export(src, { includeSecrets: false });
62
+ assert.deepEqual(out.data.files.map((f) => f.relativePath).sort(), ['coding.md', 'shared/inner.md']);
63
+ assert.equal(out.warnings.some((w) => w.includes('链接目录')), true, `缺「已跟随链接」告警: ${out.warnings.join(' | ')}`);
64
+ assert.equal(out.warnings.some((w) => w.includes('未进备份')), true, `缺「跳过链接」告警: ${out.warnings.join(' | ')}`);
65
+ assert.equal(out.warnings.some((w) => w.includes('skills/broken')), true, '必须点名被跳过的链接');
66
+ });
67
+
68
+ test('agentPresets: 目录 bundle 文件收集与写入(.agent-presets 基准目录)', async () => {
69
+ const src = makeContext('win32', 'C:\\Users\\alice');
70
+ await src.fs.writeFile('.agent-presets/work/agent.cordis.yml', Buffer.from('services:\n - name: work\n', 'utf8'));
71
+ await src.fs.writeFile('.agent-presets/work/preset.yml', Buffer.from('name: work\n', 'utf8'));
72
+
73
+ const adapter = new AgentPresetsAdapter();
74
+ const out = await adapter.export(src, { includeSecrets: false });
75
+ assert.equal(out.data.files.length, 2);
76
+ const rels = out.data.files.map((f) => f.relativePath).sort();
77
+ assert.deepEqual(rels, ['work/agent.cordis.yml', 'work/preset.yml']);
78
+
79
+ const sections = new Map([['agentPresets', out.data]]);
80
+ const dst = makeContext('linux', '/home/bob');
81
+ const items = await adapter.analyzeImport(out.data, makeImportContext(dst, sections));
82
+ assert.ok(items.every((i) => i.kind === 'Create'));
83
+ for (const item of items) {
84
+ await adapter.applyItem(item, makeImportContext(dst, sections));
85
+ }
86
+ assert.equal(
87
+ Buffer.from(await dst.fs.readFile('.agent-presets/work/agent.cordis.yml')).toString(),
88
+ 'services:\n - name: work\n',
89
+ );
90
+ });
91
+
92
+ test('agentInstructions: 只收集 homeDir 根 AGENTS.md(白名单)+ 导入往返', async () => {
93
+ const src = makeContext('win32', 'C:\\Users\\alice');
94
+ await src.fs.writeFile('AGENTS.md', Buffer.from('# Global rules\nAlways reply in Chinese.\n', 'utf8'));
95
+ // 根目录其他文件不得被收集(不能整目录递归)
96
+ await src.fs.writeFile('settings.yaml', Buffer.from('foo: bar\n', 'utf8'));
97
+
98
+ const adapter = new AgentInstructionsAdapter();
99
+ assert.equal(adapter.defaultIncluded, true, 'agentInstructions 默认导出');
100
+ assert.equal(adapter.portability, 'portable');
101
+ const out = await adapter.export(src, { includeSecrets: false });
102
+ assert.equal(out.data.files.length, 1);
103
+ assert.equal(out.data.files[0]?.relativePath, 'AGENTS.md');
104
+ assert.equal(out.data.files[0]?.contentHash, sha256Hex(Buffer.from('# Global rules\nAlways reply in Chinese.\n')));
105
+ assert.equal(out.warnings.length, 0, '存在文件时不告警');
106
+
107
+ const sections = new Map([['agentInstructions', out.data]]);
108
+ const dst = makeContext('linux', '/home/bob');
109
+ const items = await adapter.analyzeImport(out.data, makeImportContext(dst, sections));
110
+ assert.equal(items.length, 1);
111
+ assert.equal(items[0]?.kind, 'Create');
112
+ const r = await adapter.applyItem(items[0]!, makeImportContext(dst, sections));
113
+ assert.equal(r.ok, true);
114
+ assert.equal(
115
+ Buffer.from(await dst.fs.readFile('AGENTS.md')).toString(),
116
+ '# Global rules\nAlways reply in Chinese.\n',
117
+ '导入写回 $DSH_HOME/AGENTS.md(homeDir 根)',
118
+ );
119
+
120
+ // 幂等:一致 → Skip;内容不同 → Conflict
121
+ let items2 = await adapter.analyzeImport(out.data, makeImportContext(dst, sections));
122
+ assert.equal(items2[0]?.kind, 'Skip');
123
+ await dst.fs.writeFile('AGENTS.md', Buffer.from('# Changed\n', 'utf8'));
124
+ items2 = await adapter.analyzeImport(out.data, makeImportContext(dst, sections));
125
+ assert.equal(items2[0]?.kind, 'Conflict');
126
+ });
127
+
128
+ test('agentInstructions: 文件缺失 → 空分区 + dirEmpty 警告', async () => {
129
+ const src = makeContext('win32', 'C:\\Users\\alice');
130
+ const adapter = new AgentInstructionsAdapter();
131
+ const out = await adapter.export(src, { includeSecrets: false });
132
+ assert.equal(out.data.files.length, 0);
133
+ assert.ok(out.warnings.length > 0, '缺失时给出提示(与 skills 目录为空一致)');
134
+ });
135
+
136
+ test('pluginFiles: 白名单导出(不存在跳过)+ 默认不包含', async () => {
137
+ const src = makeContext('win32', 'C:\\Users\\alice');
138
+ await src.fs.writeFile('dsh-ssh.json', Buffer.from('{"hosts":[]}', 'utf8'));
139
+ await src.fs.writeFile('other-file.json', Buffer.from('{"x":1}', 'utf8'));
140
+
141
+ const adapter = new PluginFilesAdapter(['dsh-ssh.json', 'pet.json']);
142
+ assert.equal(adapter.defaultIncluded, false, 'pluginFiles 默认不导出');
143
+ const out = await adapter.export(src, { includeSecrets: false });
144
+ assert.equal(out.data.files.length, 1);
145
+ assert.equal(out.data.files[0]?.relativePath, 'dsh-ssh.json');
146
+ assert.ok(!out.data.files.some((f) => f.relativePath === 'other-file.json'), '白名单外文件不导出');
147
+
148
+ const sections = new Map([['pluginFiles', out.data]]);
149
+ const dst = makeContext('linux', '/home/bob');
150
+ const items = await adapter.analyzeImport(out.data, makeImportContext(dst, sections));
151
+ assert.equal(items[0]?.kind, 'Create');
152
+ await adapter.applyItem(items[0]!, makeImportContext(dst, sections));
153
+ assert.equal(Buffer.from(await dst.fs.readFile('dsh-ssh.json')).toString(), '{"hosts":[]}');
154
+ });
155
+
156
+ test('pluginFiles: 约定配置目录递归收集 + 与白名单去重 + 导入映射写回', async () => {
157
+ const src = makeContext('win32', 'C:\\Users\\alice');
158
+ // 约定目录下的文件(含嵌套子目录)
159
+ await src.fs.writeFile('plugin-config/pluginA/a.json', Buffer.from('{"a":1}', 'utf8'));
160
+ await src.fs.writeFile('plugin-config/pluginA/sub/b.toml', Buffer.from('b=2', 'utf8'));
161
+ // 白名单文件
162
+ await src.fs.writeFile('dsh-ssh.json', Buffer.from('{"hosts":[]}', 'utf8'));
163
+ // 白名单里显式点到的文件同时位于约定目录内 → 应去重,只收集一次
164
+ //(whitelist 用与 collectDir 相同的相对路径 plugin-config/pluginA/a.json)
165
+
166
+ // whitelist 命中约定目录内同路径文件:a.json 同时由白名单与目录收集 → 去重
167
+ const whitelist = ['dsh-ssh.json', 'plugin-config/pluginA/a.json'];
168
+ const adapter = new PluginFilesAdapter(whitelist, 'plugin-config');
169
+ const out = await adapter.export(src, { includeSecrets: false });
170
+ const rels = out.data.files.map((f) => f.relativePath).sort();
171
+ // 白名单 dsh-ssh.json + 目录 a.json、sub/b.toml(a.json 白名单/目录交叉去重,只出现一次)
172
+ assert.deepEqual(rels, ['dsh-ssh.json', 'plugin-config/pluginA/a.json', 'plugin-config/pluginA/sub/b.toml']);
173
+
174
+ const sections = new Map([['pluginFiles', out.data]]);
175
+ const dst = makeContext('linux', '/home/bob');
176
+ const items = await adapter.analyzeImport(out.data, makeImportContext(dst, sections));
177
+ assert.equal(items.length, 3);
178
+ assert.ok(items.every((i) => i.kind === 'Create'));
179
+ for (const item of items) {
180
+ await adapter.applyItem(item, makeImportContext(dst, sections));
181
+ }
182
+ assert.equal(Buffer.from(await dst.fs.readFile('dsh-ssh.json')).toString(), '{"hosts":[]}');
183
+ assert.equal(Buffer.from(await dst.fs.readFile('plugin-config/pluginA/sub/b.toml')).toString(), 'b=2');
184
+ assert.equal(Buffer.from(await dst.fs.readFile('plugin-config/pluginA/a.json')).toString(), '{"a":1}');
185
+ });
186
+
187
+ test('pluginFiles: 非法 collectDir(绝对路径/越界)构造即抛错', () => {
188
+ assert.throws(() => new PluginFilesAdapter(undefined, 'C:\\evil'), /collectDir 非法/);
189
+ assert.throws(() => new PluginFilesAdapter(undefined, '../escape'), /collectDir 非法/);
190
+ });
191
+
192
+ test('sessions: 默认关 + 文件级复制(deviceSpecific)', async () => {
193
+ const src = makeContext('win32', 'C:\\Users\\alice');
194
+ await src.fs.writeFile('sessions/proj-a/s1/session.jsonl.zstd', Buffer.from('zstd-bytes', 'utf8'));
195
+ const adapter = new SessionsAdapter();
196
+ assert.equal(adapter.defaultIncluded, false, 'sessions 默认不导出');
197
+ assert.equal(adapter.portability, 'deviceSpecific');
198
+ const out = await adapter.export(src, { includeSecrets: false });
199
+ assert.equal(out.data.files.length, 1);
200
+ assert.equal(out.data.files[0]?.relativePath, 'proj-a/s1/session.jsonl.zstd');
201
+
202
+ const sections = new Map([['sessions', out.data]]);
203
+ const dst = makeContext('linux', '/home/bob');
204
+ const items = await adapter.analyzeImport(out.data, makeImportContext(dst, sections));
205
+ const r = await adapter.applyItem(items[0]!, makeImportContext(dst, sections));
206
+ assert.equal(r.ok, true);
207
+ assert.equal(Buffer.from(await dst.fs.readFile('sessions/proj-a/s1/session.jsonl.zstd')).toString(), 'zstd-bytes');
208
+ });
209
+
210
+ test('F23: pluginFiles 拒绝 `..` 路径穿越变体写入保留区(Reviewer B P0)', async () => {
211
+ const adapter = new PluginFilesAdapter();
212
+ const dst = makeContext('linux', '/home/bob');
213
+ // 宿主 fs 会 resolve/join 折叠 `..` → 这些路径落到保留区,必须在 adapter 层拒绝
214
+ const malicious = [
215
+ 'dsh-config-manager/../dsh-config-manager/snapshots/fake/snapshot.json',
216
+ 'dsh-config-manager/../dsh-config-manager/transactions/active/x.json',
217
+ 'x/../../dsh-config-manager/locks/environment.lock',
218
+ 'dsh-config-manager/./snapshots/fake/blob-1',
219
+ ];
220
+ const data = {
221
+ version: 1 as const,
222
+ files: malicious.map((relativePath) => ({
223
+ relativePath,
224
+ data: Buffer.from('{}', 'utf8'),
225
+ contentHash: sha256Hex(Buffer.from('{}')),
226
+ })),
227
+ };
228
+ const ctx = makeImportContext(dst, new Map([['pluginFiles', data]]));
229
+ const items = await adapter.analyzeImport(data, ctx);
230
+ for (const item of items) {
231
+ assert.equal(item.kind, 'Error', `analyzeImport 应拒绝 ${item.target?.ref ?? item.description}`);
232
+ const r = await adapter.applyItem(item, ctx);
233
+ assert.equal(r.ok, false);
234
+ }
235
+ // 纵深防御:apply 阶段直接命中也必须拒绝
236
+ for (const rel of malicious) {
237
+ const r = await adapter.applyItem(
238
+ { id: `x:${rel}`, kind: 'Create', adapter: 'pluginFiles', description: rel, severity: 'info', target: { adapter: 'pluginFiles', ref: rel } } as PlanItem,
239
+ ctx,
240
+ );
241
+ assert.equal(r.ok, false, `applyItem 拒绝 .. 变体 ${rel}`);
242
+ }
243
+ });
244
+
245
+ test('文件类 validate', async () => {
246
+ const adapter = new SkillsAdapter();
247
+ assert.equal((await adapter.validate({ version: 1, files: [] })).valid, true);
248
+ assert.equal((await adapter.validate({ version: 1, files: [{ relativePath: '' } as never] })).valid, false);
249
+ });
250
+
251
+ // ---------- F23:不可信 import 不得写内部 control-plane namespace ----------
252
+
253
+ test('F23: pluginFiles 拒绝写内部 recovery/control-plane 保留区(快照/事务/锁)', async () => {
254
+ const adapter = new PluginFilesAdapter();
255
+ const src = makeContext('win32', 'C:\\Users\\alice');
256
+ const malicious = [
257
+ 'dsh-config-manager/snapshots/fake/snapshot.json',
258
+ 'dsh-config-manager/snapshots/fake/blob-1',
259
+ 'dsh-config-manager/transactions/active/x.json',
260
+ 'dsh-config-manager/transactions/safe-mode',
261
+ 'dsh-config-manager/locks/environment.lock',
262
+ 'dsh-config-manager/environment-fingerprint.token',
263
+ ];
264
+ // 攻击者构造 ZIP 数据:把普通插件文件条目映射到内部 recovery 存储
265
+ const data = {
266
+ version: 1 as const,
267
+ files: malicious.map((relativePath) => ({
268
+ relativePath,
269
+ data: Buffer.from('{}', 'utf8'),
270
+ contentHash: sha256Hex(Buffer.from('{}')),
271
+ })),
272
+ };
273
+ const sections = new Map([['pluginFiles', data]]);
274
+ const ctx = makeImportContext(src, sections);
275
+ const items = await adapter.analyzeImport(data, ctx);
276
+ for (const item of items) {
277
+ assert.equal(item.kind, 'Error', `analyzeImport 应拒绝 ${item.target?.ref ?? item.description}`);
278
+ const r = await adapter.applyItem(item, ctx);
279
+ assert.equal(r.ok, false, `applyItem 应拒绝 ${item.target?.ref ?? item.description}`);
280
+ }
281
+ // 纵深防御:应用阶段直接命中保留区也必须拒绝(即使 analyzeImport 被绕过)
282
+ for (const rel of malicious) {
283
+ const r = await adapter.applyItem(
284
+ { id: `x:${rel}`, kind: 'Create', adapter: 'pluginFiles', description: rel, severity: 'info', target: { adapter: 'pluginFiles', ref: rel } } as PlanItem,
285
+ ctx,
286
+ );
287
+ assert.equal(r.ok, false, `applyItem 直接命中 ${rel} 应拒绝`);
288
+ }
289
+ // 关键:不得真正写入 control-plane 存储
290
+ for (const rel of malicious) {
291
+ assert.equal(await src.fs.exists(rel), false, `不得写入保留路径 ${rel}`);
292
+ }
293
+ });
294
+
295
+ test('F23: self adapter 拒绝写内部 recovery/control-plane 保留区(但放行合法配置)', async () => {
296
+ const adapter = new SelfAdapter('dsh-config-manager');
297
+ const dst = makeContext('linux', '/home/bob');
298
+ // 合法:sync-config.json 是 self 白名单配置,必须放行
299
+ const legitFiles = [
300
+ 'sync/sync-config.json',
301
+ 'sync/sync-selection.json',
302
+ 'sync/ui-prefs.json',
303
+ 'sync/backup-schedule.json',
304
+ 'market/market-config.json',
305
+ 'exports/.backup-notes.json',
306
+ ];
307
+ const legitData = {
308
+ version: 1 as const,
309
+ files: legitFiles.map((relativePath) => ({
310
+ relativePath,
311
+ data: Buffer.from('{}', 'utf8'),
312
+ contentHash: sha256Hex(Buffer.from('{}')),
313
+ })),
314
+ };
315
+ const legitSections = new Map([['self', legitData]]);
316
+ const legitCtx = makeImportContext(dst, legitSections);
317
+ const legitItems = await adapter.analyzeImport(legitData, legitCtx);
318
+ assert.ok(legitItems.every((i) => i.kind !== 'Error'), '合法 self 配置不得被保留区误伤');
319
+ for (const item of legitItems) {
320
+ const r = await adapter.applyItem(item, legitCtx);
321
+ if (r.ok === false) continue; // 同名冲突等非 F23 因素允许
322
+ }
323
+
324
+ // 恶意:把条目映射到内部 recovery 存储
325
+ const malicious = [
326
+ 'snapshots/fake/snapshot.json',
327
+ 'transactions/active/x.json',
328
+ 'locks/environment.lock',
329
+ 'sync/snapshots/fake/manifest.json', // sync rollback snapshot store
330
+ 'sync/work/tmp.zip',
331
+ ];
332
+ const evilData = {
333
+ version: 1 as const,
334
+ files: malicious.map((relativePath) => ({
335
+ relativePath,
336
+ data: Buffer.from('{}', 'utf8'),
337
+ contentHash: sha256Hex(Buffer.from('{}')),
338
+ })),
339
+ };
340
+ const evilSections = new Map([['self', evilData]]);
341
+ const evilCtx = makeImportContext(dst, evilSections);
342
+ const evilItems = await adapter.analyzeImport(evilData, evilCtx);
343
+ assert.equal(evilItems.length, malicious.length);
344
+ for (const item of evilItems) {
345
+ assert.equal(item.kind, 'Error', `self analyzeImport 应拒绝 ${item.target?.ref ?? item.description}`);
346
+ const r = await adapter.applyItem(item, evilCtx);
347
+ assert.equal(r.ok, false);
348
+ }
349
+ const resolved = malicious.map((rel) => `dsh-config-manager/${rel}`);
350
+ for (const rel of resolved) {
351
+ assert.equal(await dst.fs.exists(rel), false, `不得写入保留路径 ${rel}`);
352
+ }
353
+ });