dsh-config-manager 0.1.58 → 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 (323) hide show
  1. package/README.md +41 -2
  2. package/README.zh-CN.md +42 -0
  3. package/lib/adapters/file-collection.js +7 -2
  4. package/lib/adapters/index.d.ts +7 -0
  5. package/lib/adapters/index.js +1 -1
  6. package/lib/adapters/link-report.d.ts +16 -0
  7. package/lib/adapters/link-report.js +47 -0
  8. package/lib/adapters/plugin-files.js +7 -4
  9. package/lib/adapters/plugins.d.ts +24 -2
  10. package/lib/adapters/plugins.js +329 -9
  11. package/lib/adapters/pnpm-workspace.d.ts +26 -0
  12. package/lib/adapters/pnpm-workspace.js +147 -0
  13. package/lib/adapters/test-helpers.d.ts +2 -0
  14. package/lib/adapters/test-helpers.js +2 -0
  15. package/lib/cli/index.d.ts +11 -1
  16. package/lib/cli/index.js +325 -4
  17. package/lib/client.d.ts +293 -1
  18. package/lib/client.js +2032 -839
  19. package/lib/core/analyzer.d.ts +20 -2
  20. package/lib/core/analyzer.js +138 -32
  21. package/lib/core/backup-plan.d.ts +69 -0
  22. package/lib/core/backup-plan.js +263 -0
  23. package/lib/core/backup-verify.d.ts +24 -0
  24. package/lib/core/backup-verify.js +271 -0
  25. package/lib/core/backup.d.ts +58 -2
  26. package/lib/core/backup.js +50 -8
  27. package/lib/core/boot-rescue.d.ts +146 -0
  28. package/lib/core/boot-rescue.js +618 -0
  29. package/lib/core/config-lifecycle.d.ts +180 -0
  30. package/lib/core/config-lifecycle.js +481 -0
  31. package/lib/core/config-snapshot.d.ts +135 -0
  32. package/lib/core/config-snapshot.js +424 -0
  33. package/lib/core/config-state.d.ts +70 -0
  34. package/lib/core/config-state.js +177 -0
  35. package/lib/core/crash-report.d.ts +84 -0
  36. package/lib/core/crash-report.js +317 -0
  37. package/lib/core/exporter.d.ts +9 -0
  38. package/lib/core/exporter.js +110 -2
  39. package/lib/core/journal.d.ts +3 -0
  40. package/lib/core/local-plugin-host.d.ts +35 -0
  41. package/lib/core/local-plugin-host.js +120 -0
  42. package/lib/core/local-plugin-pack.d.ts +135 -0
  43. package/lib/core/local-plugin-pack.js +278 -0
  44. package/lib/core/messages.d.ts +28 -0
  45. package/lib/core/messages.js +56 -0
  46. package/lib/core/model-tools.d.ts +8 -1
  47. package/lib/core/model-tools.js +2 -0
  48. package/lib/core/phase3-host.js +1 -0
  49. package/lib/core/plugin-cli.d.ts +1 -1
  50. package/lib/core/plugin-cli.js +35 -0
  51. package/lib/core/recovery-orchestrator.d.ts +23 -0
  52. package/lib/core/recovery-orchestrator.js +47 -2
  53. package/lib/core/types.d.ts +32 -0
  54. package/lib/core/undo.d.ts +73 -0
  55. package/lib/core/undo.js +87 -0
  56. package/lib/core/watcher.d.ts +101 -0
  57. package/lib/core/watcher.js +219 -0
  58. package/lib/index.d.ts +1 -1
  59. package/lib/index.js +448 -34
  60. package/lib/market/prepare.js +18 -0
  61. package/lib/market/security.js +27 -0
  62. package/lib/schema/config.js +25 -0
  63. package/lib/schema/index.d.ts +24 -0
  64. package/lib/schema/index.js +26 -0
  65. package/lib/schema/types.d.ts +49 -0
  66. package/lib/security/encryption.d.ts +0 -5
  67. package/lib/security/encryption.js +4 -11
  68. package/lib/sync/autosync-scheduler.js +22 -0
  69. package/lib/sync/backup-files.d.ts +14 -0
  70. package/lib/sync/backup-files.js +37 -0
  71. package/lib/sync/backup-schedule-config.d.ts +13 -0
  72. package/lib/sync/backup-schedule-config.js +26 -0
  73. package/lib/sync/backup-scheduler.d.ts +19 -4
  74. package/lib/sync/backup-scheduler.js +37 -12
  75. package/lib/sync/retention-policy.d.ts +143 -0
  76. package/lib/sync/retention-policy.js +237 -0
  77. package/lib/ui/backup-schedule.d.ts +30 -2
  78. package/lib/ui/backup-schedule.js +33 -0
  79. package/lib/ui/i18n.d.ts +1 -0
  80. package/lib/ui/i18n.js +2 -0
  81. package/lib/ui/test-helpers.js +2 -0
  82. package/lib/ui/types.d.ts +26 -0
  83. package/lib/utils/bundle-scan.d.ts +133 -0
  84. package/lib/utils/bundle-scan.js +297 -0
  85. package/lib/utils/env-lock.d.ts +30 -0
  86. package/lib/utils/env-lock.js +93 -7
  87. package/lib/utils/recursive-walk.d.ts +24 -0
  88. package/lib/utils/recursive-walk.js +134 -0
  89. package/package.json +57 -6
  90. package/src/adapters/file-collection.ts +8 -2
  91. package/src/adapters/files.test.ts +19 -0
  92. package/src/adapters/index.ts +8 -2
  93. package/src/adapters/link-report.test.ts +65 -0
  94. package/src/adapters/link-report.ts +61 -0
  95. package/src/adapters/plugin-files.ts +8 -4
  96. package/src/adapters/plugins.test.ts +129 -0
  97. package/src/adapters/plugins.ts +343 -10
  98. package/src/adapters/pnpm-workspace.test.ts +87 -0
  99. package/src/adapters/pnpm-workspace.ts +150 -0
  100. package/src/adapters/test-helpers.ts +2 -0
  101. package/src/cli/index.ts +357 -6
  102. package/src/client/ConfigManagerSection.tsx +37 -2
  103. package/src/client/bundle-selfcontained.test.ts +85 -0
  104. package/src/client/client-types.ts +2 -0
  105. package/src/client/import/ImportWizardView.tsx +1 -1
  106. package/src/client/index.ts +4 -1
  107. package/src/client/lifecycle/LifecyclePanel.tsx +524 -0
  108. package/src/client/lifecycle/lifecycle-api.ts +249 -0
  109. package/src/client/locales.ts +138 -0
  110. package/src/client/market/market-view.test.ts +2 -0
  111. package/src/client/overview/OverviewPanel.tsx +7 -1
  112. package/src/client/recovery/RecoveryPanel.tsx +53 -1
  113. package/src/client/recovery/recovery-api.ts +12 -1
  114. package/src/client/recovery/recovery-locales.ts +19 -0
  115. package/src/client/recovery/recovery-view.test.ts +23 -0
  116. package/src/client/recovery/recovery-view.ts +19 -3
  117. package/src/client/run-store.test.ts +48 -13
  118. package/src/client/run-store.ts +8 -1
  119. package/src/client/snapshots/SnapshotsPanel.tsx +115 -6
  120. package/src/client/sync/history-model.test.ts +10 -0
  121. package/src/client/sync/history-model.ts +4 -0
  122. package/src/client/sync/sync-view-v2.test.ts +30 -2
  123. package/src/client/sync/sync-view.ts +19 -3
  124. package/src/core/analyzer.ts +167 -34
  125. package/src/core/backup-plan.ts +327 -0
  126. package/src/core/backup-verify.ts +306 -0
  127. package/src/core/backup.ts +93 -7
  128. package/src/core/boot-rescue.test.ts +847 -0
  129. package/src/core/boot-rescue.ts +737 -0
  130. package/src/core/config-lifecycle.test.ts +727 -0
  131. package/src/core/config-lifecycle.ts +573 -0
  132. package/src/core/config-snapshot.test.ts +369 -0
  133. package/src/core/config-snapshot.ts +522 -0
  134. package/src/core/config-state.test.ts +381 -0
  135. package/src/core/config-state.ts +207 -0
  136. package/src/core/crash-report.test.ts +380 -0
  137. package/src/core/crash-report.ts +369 -0
  138. package/src/core/exporter.ts +110 -2
  139. package/src/core/journal.ts +3 -0
  140. package/src/core/local-plugin-host.ts +152 -0
  141. package/src/core/local-plugin-pack.test.ts +359 -0
  142. package/src/core/local-plugin-pack.ts +363 -0
  143. package/src/core/messages.ts +56 -0
  144. package/src/core/model-tools.test.ts +92 -2
  145. package/src/core/model-tools.ts +10 -1
  146. package/src/core/phase1-wiring.test.ts +139 -0
  147. package/src/core/phase3-host.ts +1 -0
  148. package/src/core/phase3-p2b-fingerprint.test.ts +8 -0
  149. package/src/core/plugin-cli.fs.test.ts +31 -0
  150. package/src/core/plugin-cli.test.ts +11 -0
  151. package/src/core/plugin-cli.ts +34 -0
  152. package/src/core/recovery-orchestrator.test.ts +180 -0
  153. package/src/core/recovery-orchestrator.ts +60 -2
  154. package/src/core/types.ts +29 -0
  155. package/src/core/undo.ts +116 -0
  156. package/src/core/watcher.test.ts +326 -0
  157. package/src/core/watcher.ts +263 -0
  158. package/src/index.ts +404 -33
  159. package/src/market/market.test.ts +44 -0
  160. package/src/market/prepare.test.ts +107 -0
  161. package/src/market/prepare.ts +22 -0
  162. package/src/market/security.ts +27 -0
  163. package/src/schema/config.ts +24 -0
  164. package/src/schema/index.ts +71 -0
  165. package/src/schema/manifest-schema.test.ts +519 -0
  166. package/src/schema/types.ts +52 -0
  167. package/src/security/encryption.ts +4 -12
  168. package/src/security/security.test.ts +3 -6
  169. package/src/sync/autosync-scheduler.test.ts +70 -3
  170. package/src/sync/autosync-scheduler.ts +21 -0
  171. package/src/sync/backup-files.ts +42 -0
  172. package/src/sync/backup-schedule-config.ts +32 -0
  173. package/src/sync/backup-scheduler.test.ts +316 -3
  174. package/src/sync/backup-scheduler.ts +53 -16
  175. package/src/sync/retention-policy.test.ts +418 -0
  176. package/src/sync/retention-policy.ts +286 -0
  177. package/src/sync/sync-session.test.ts +2 -0
  178. package/src/ui/backup-inspect.test.ts +2 -0
  179. package/src/ui/backup-schedule.test.ts +75 -0
  180. package/src/ui/backup-schedule.ts +43 -2
  181. package/src/ui/i18n.ts +2 -0
  182. package/src/ui/test-helpers.ts +3 -1
  183. package/src/ui/types.ts +28 -0
  184. package/src/utils/bundle-scan.test.ts +187 -0
  185. package/src/utils/bundle-scan.ts +335 -0
  186. package/src/utils/env-lock.test.ts +131 -0
  187. package/src/utils/env-lock.ts +93 -6
  188. package/src/utils/recursive-walk.test.ts +148 -0
  189. package/src/utils/recursive-walk.ts +145 -0
  190. package/lib/adapters/agent-instructions.js.map +0 -1
  191. package/lib/adapters/agent-presets.js.map +0 -1
  192. package/lib/adapters/credentials.js.map +0 -1
  193. package/lib/adapters/file-collection.js.map +0 -1
  194. package/lib/adapters/index.js.map +0 -1
  195. package/lib/adapters/mcp.js.map +0 -1
  196. package/lib/adapters/plugin-files.js.map +0 -1
  197. package/lib/adapters/plugins.js.map +0 -1
  198. package/lib/adapters/prompts.js.map +0 -1
  199. package/lib/adapters/providers.js.map +0 -1
  200. package/lib/adapters/self.js.map +0 -1
  201. package/lib/adapters/sessions.js.map +0 -1
  202. package/lib/adapters/settings.js.map +0 -1
  203. package/lib/adapters/skills.js.map +0 -1
  204. package/lib/adapters/test-helpers.js.map +0 -1
  205. package/lib/adapters/ui.js.map +0 -1
  206. package/lib/adapters/workspaces.js.map +0 -1
  207. package/lib/cli/index.js.map +0 -1
  208. package/lib/client.js.map +0 -1
  209. package/lib/core/analyzer.js.map +0 -1
  210. package/lib/core/backup.js.map +0 -1
  211. package/lib/core/cache-cleaner.js.map +0 -1
  212. package/lib/core/consult-source.js.map +0 -1
  213. package/lib/core/exporter.js.map +0 -1
  214. package/lib/core/ghost-sweep.js.map +0 -1
  215. package/lib/core/importer.js.map +0 -1
  216. package/lib/core/index.js.map +0 -1
  217. package/lib/core/journal.js.map +0 -1
  218. package/lib/core/messages.js.map +0 -1
  219. package/lib/core/migration-consult.js.map +0 -1
  220. package/lib/core/migration-history.js.map +0 -1
  221. package/lib/core/model-tools.js.map +0 -1
  222. package/lib/core/msg-types.js.map +0 -1
  223. package/lib/core/phase3-child-crash.js.map +0 -1
  224. package/lib/core/phase3-host.js.map +0 -1
  225. package/lib/core/phase3-prod-child.js.map +0 -1
  226. package/lib/core/phase4-crash-child.js.map +0 -1
  227. package/lib/core/plugin-cli.js.map +0 -1
  228. package/lib/core/reconcile.js.map +0 -1
  229. package/lib/core/recovery-orchestrator.js.map +0 -1
  230. package/lib/core/reinstall.js.map +0 -1
  231. package/lib/core/restore.js.map +0 -1
  232. package/lib/core/rollback.js.map +0 -1
  233. package/lib/core/run-registry.js.map +0 -1
  234. package/lib/core/startup-barrier.js.map +0 -1
  235. package/lib/core/transaction-coordinator.js.map +0 -1
  236. package/lib/core/types.js.map +0 -1
  237. package/lib/core/validator.js.map +0 -1
  238. package/lib/core/verify-recovery.js.map +0 -1
  239. package/lib/index.js.map +0 -1
  240. package/lib/market/builtin.js.map +0 -1
  241. package/lib/market/git-file-writer.js.map +0 -1
  242. package/lib/market/github-repos.js.map +0 -1
  243. package/lib/market/index-parser.js.map +0 -1
  244. package/lib/market/market-config.js.map +0 -1
  245. package/lib/market/my-repo.js.map +0 -1
  246. package/lib/market/prepare.js.map +0 -1
  247. package/lib/market/reader.js.map +0 -1
  248. package/lib/market/repo-url.js.map +0 -1
  249. package/lib/market/security.js.map +0 -1
  250. package/lib/market/star-cache.js.map +0 -1
  251. package/lib/market/types.js.map +0 -1
  252. package/lib/market/upstream.js.map +0 -1
  253. package/lib/market/url.js.map +0 -1
  254. package/lib/market/view.js.map +0 -1
  255. package/lib/migrations/index.js.map +0 -1
  256. package/lib/migrations/v1-to-v2.js.map +0 -1
  257. package/lib/profiles/index.js.map +0 -1
  258. package/lib/profiles/profile-manager.js.map +0 -1
  259. package/lib/schema/config.js.map +0 -1
  260. package/lib/schema/manifest.js.map +0 -1
  261. package/lib/schema/tombstones.js.map +0 -1
  262. package/lib/schema/types.js.map +0 -1
  263. package/lib/schema/versions.js.map +0 -1
  264. package/lib/security/encryption.js.map +0 -1
  265. package/lib/security/index.js.map +0 -1
  266. package/lib/security/integrity.js.map +0 -1
  267. package/lib/security/redaction.js.map +0 -1
  268. package/lib/security/secret-scanner.js.map +0 -1
  269. package/lib/security/vault.js.map +0 -1
  270. package/lib/security/zip-security.js.map +0 -1
  271. package/lib/sync/ancestor.js.map +0 -1
  272. package/lib/sync/autosync-config.js.map +0 -1
  273. package/lib/sync/autosync-scheduler.js.map +0 -1
  274. package/lib/sync/backup-files.js.map +0 -1
  275. package/lib/sync/backup-schedule-config.js.map +0 -1
  276. package/lib/sync/backup-scheduler.js.map +0 -1
  277. package/lib/sync/fs.js.map +0 -1
  278. package/lib/sync/git/git-transport.js.map +0 -1
  279. package/lib/sync/github-auth.js.map +0 -1
  280. package/lib/sync/layout.js.map +0 -1
  281. package/lib/sync/merge.js.map +0 -1
  282. package/lib/sync/review-queue.js.map +0 -1
  283. package/lib/sync/risk.js.map +0 -1
  284. package/lib/sync/snapshot-crypto.js.map +0 -1
  285. package/lib/sync/snapshot-json.js.map +0 -1
  286. package/lib/sync/sync-config.js.map +0 -1
  287. package/lib/sync/sync-engine.js.map +0 -1
  288. package/lib/sync/sync-history.js.map +0 -1
  289. package/lib/sync/sync-selection.js.map +0 -1
  290. package/lib/sync/sync-session.js.map +0 -1
  291. package/lib/sync/sync-state.js.map +0 -1
  292. package/lib/sync/transport.js.map +0 -1
  293. package/lib/sync/ui-prefs.js.map +0 -1
  294. package/lib/sync/webdav/webdav-transport.js.map +0 -1
  295. package/lib/ui/backup-inspect.js.map +0 -1
  296. package/lib/ui/backup-schedule.js.map +0 -1
  297. package/lib/ui/conflict-view.js.map +0 -1
  298. package/lib/ui/errors.js.map +0 -1
  299. package/lib/ui/export-flow.js.map +0 -1
  300. package/lib/ui/flow.js.map +0 -1
  301. package/lib/ui/history-model.js.map +0 -1
  302. package/lib/ui/i18n.js.map +0 -1
  303. package/lib/ui/import-stepper.js.map +0 -1
  304. package/lib/ui/import-wizard.js.map +0 -1
  305. package/lib/ui/migration-consult-view.js.map +0 -1
  306. package/lib/ui/next-steps.js.map +0 -1
  307. package/lib/ui/overview-view.js.map +0 -1
  308. package/lib/ui/path-mapping.js.map +0 -1
  309. package/lib/ui/profiles-view.js.map +0 -1
  310. package/lib/ui/progress.js.map +0 -1
  311. package/lib/ui/release-notes-prompt.js.map +0 -1
  312. package/lib/ui/report.js.map +0 -1
  313. package/lib/ui/star-prompt.js.map +0 -1
  314. package/lib/ui/test-helpers.js.map +0 -1
  315. package/lib/ui/types.js.map +0 -1
  316. package/lib/utils/atomic-write.js.map +0 -1
  317. package/lib/utils/env-lock.js.map +0 -1
  318. package/lib/utils/hashing.js.map +0 -1
  319. package/lib/utils/json.js.map +0 -1
  320. package/lib/utils/logger.js.map +0 -1
  321. package/lib/utils/paths.js.map +0 -1
  322. package/lib/utils/proxy.js.map +0 -1
  323. package/lib/utils/zip.js.map +0 -1
package/README.md CHANGED
@@ -317,6 +317,10 @@ dsh-config-manager restore [--id <id>] [--dry-run]
317
317
  dsh-config-manager reinstall [--version <v>] [--yes] [--list]
318
318
  [--wipe-config] [--dry-run] # one-click reinstall of DSH itself
319
319
  dsh-config-manager recover-stale-lock [--data-dir <dir>] # clear a leftover lock (see below)
320
+ dsh-config-manager verify [--id <file|path>] [--json] # read-only check of backup ZIPs
321
+ [--data-dir <dir>]
322
+ dsh-config-manager backup [--sections <a,b,c>] [--out <path>] # offline file-level backup
323
+ [--dry-run] [--data-dir <dir>]
320
324
  ```
321
325
 
322
326
  **`reinstall` — rescue when DSH is broken.** It reinstalls the `@deepseek-ai/dsh` launcher across platforms (uses the right command per OS: PowerShell on Windows, bash on Unix). By default it reinstalls the launcher + clears global caches; interactively it asks which **dangerous** clean-up items to include (settings / plugins / session data & credentials) — those are **not** selected by default, and any destructive choice requires a second confirmation by typing `YES` before anything runs. Before wiping any `~/.dsh` data it makes an emergency backup at `.reinstall-backup` (the `snapshots/` folder is deliberately never touched).
@@ -348,6 +352,40 @@ dsh-config-manager restore --id <snapshot-id> # execute (current
348
352
 
349
353
  Every overwrite/delete is first copied to `<snapshotDir>/pre-restore/` so you can manually change your mind. Exit code is `1` if any action failed; the report honestly lists restored / removedPlugins / manualHints / failed / skipped.
350
354
 
355
+ **`verify` — is that old backup still usable?** The GUI lives inside DSH, so it cannot answer this when DSH will not start; `verify` can. It re-reads a backup ZIP from disk **without writing a single byte**, then reports one verdict per file:
356
+
357
+ | Verdict | Meaning |
358
+ |---|---|
359
+ | `OK` | Structurally valid and every entry matches its SHA-256 in `integrity/checksums.json` |
360
+ | `MISSING` | The file is not there (wrong path / already deleted) |
361
+ | `CORRUPT` | Damaged or tampered — the message names the exact offending entry |
362
+ | `UNSUPPORTED` | A valid backup this plugin version cannot read (schema too new, or an encrypted container that must be decrypted first) |
363
+ | `VERIFY_ERROR` | The check itself failed (disk I/O); never downgraded to a guess |
364
+
365
+ With no argument it checks **every** `*.zip` in the exports directory; pass a file name or a path to check just one. Exit code is `1` unless **all** checked backups are `OK` — which makes it safe to assert from CI or a scheduled task. `--json` prints the same result machine-readably.
366
+
367
+ ```bash
368
+ dsh-config-manager verify # check every backup in the exports directory
369
+ dsh-config-manager verify --json # machine-readable (exit code still 0/1)
370
+ dsh-config-manager verify my-backup.zip # check a single file by name
371
+ dsh-config-manager verify C:/backups/dsh-config.zip # ...or by path
372
+ ```
373
+
374
+ **`backup` — backup even when DSH is down.** This is the offline counterpart of the GUI export: it packs the parts of `$DSH_HOME` that can be read **without the DSH runtime** (skills, agent presets, agent instructions, and the plugin’s own config) into a ZIP with the same structure as a GUI export (`manifest.json` + `integrity/checksums.json` + section directories), then immediately self-checks what it wrote with the same engine as `verify` — a backup command that never verified its own output would be worse than none.
375
+
376
+ **Credential files never enter a backup.** `.credentials.*`, `.env`, `*.pem` and friends are excluded by an explicit blacklist, only whitelisted directories are walked (never the whole home directory), and symlinks are skipped rather than followed.
377
+
378
+ Structured sections (settings / UI / providers / plugins / MCP / prompts / workspaces) are **not** silently faked: they need the DSH service layer to read and redact, so they are left out and marked `false` in the manifest — the plan printout lists them under “not offline-collectable” so you know exactly what this backup does and does not contain. Use the GUI export when DSH is healthy for a full backup.
379
+
380
+ ```bash
381
+ dsh-config-manager backup --dry-run # list what would be packed (zero writes)
382
+ dsh-config-manager backup # write into the exports directory, then self-check
383
+ dsh-config-manager backup --out D:/rescue/config.zip # explicit destination (never overwrites)
384
+ dsh-config-manager backup --sections skills,self # narrow the scope
385
+ ```
386
+
387
+ `--sections` accepts `skills,agentPresets,agentInstructions,self,pluginFiles`. `pluginFiles` is **opt-in** (as in the GUI): it copies third-party plugin files verbatim, and `dsh-ssh.json` holds plaintext host passwords — select it only when you have looked at what is in there.
388
+
351
389
  **A typical rescue flow** when DSH won't start: ① `dsh-config-manager reinstall` to bring the launcher back (plus any clean-up), ② `dsh web` to start DSH again, ③ re-add the plugin from the registry, and ④ pull a snapshot from the remote repo (or run `dsh-config-manager restore`) to bring your config back. The CLI works at every step regardless of DSH's health.
352
390
 
353
391
  **`recover-stale-lock` — when every operation suddenly fails.** Before touching your config, the plugin claims a small environment lock (it records who is operating plus a heartbeat) so that two operations can never write your config at the same time. If a `dsh web` process is **force-killed** (Task Manager, `kill -9`), the lock file survives with a dead owner: the next operation is refused, and it stays refused **no matter how often you retry or restart DSH** — because the plugin deliberately never removes a lock on its own (a wrong guess could evict a live operation).
@@ -357,14 +395,15 @@ Symptoms and the fix:
357
395
  | Symptom | Meaning | Fix |
358
396
  |---|---|---|
359
397
  | 「另一个任务正在运行,请稍后重试。」 / "Another task is running, please retry." | A live operation holds the lock | Just wait — it clears itself |
360
- | 「检测到上次异常退出残留的配置锁…重试或重启 DSH 均无效」 / relayed in the log as `自动同步已跳过` | The owner process is **proven dead** (leftover lock) | Run the command below, or use GUI **Recovery → 事故恢复** |
398
+ | 「检测到上次异常退出残留的配置锁…重试或重启 DSH 均无效」 / relayed in the log as `自动同步已跳过` | The owner process is **proven dead** (leftover lock) | Run the command below, or use GUI **Recovery → 事故恢复 → 回收残留锁** |
399
+ | Same message, but the owner PID was **reused** by an unrelated process (common on Windows) | The heartbeat has been stale for a very long time, so the lock is still classified as a leftover one | Same fix — a heartbeat that has not been refreshed for far longer than the stale window is now reclaimable |
361
400
 
362
401
  ```bash
363
402
  # safe: it inspects first and refuses unless the owner is proven dead (a live lock is never touched)
364
403
  dsh-config-manager recover-stale-lock
365
404
  ```
366
405
 
367
- **Plugins installed but the backup doesn't see them?** Check **Settings → DSH Config Manager → About**: it now shows which directory / profile the plugin list was read from, and how many plugins were detected. The list comes from `$DSH_HOME/profiles/<profile>/package.json` → `dependencies`, where `<profile>` is resolved as `config.profile` → `--profile` → `web`. If the shown path is not the profile you installed into (Desktop builds may use a different profile or a different `DSH_HOME`), that is the cause — align `--profile` / `DSH_HOME` with it.
406
+ **Plugins installed but the backup doesn't see them?** Check **Settings → DSH Config Manager → About**: it now shows which directory / profile the plugin list was read from, and how many plugins were detected. The list comes from `$DSH_HOME/profiles/<profile>/package.json` → `dependencies` (plus anything declared in `dsh.profile.bundles` that is not a dependency), where `<profile>` is resolved as `config.profile` → `--profile` → `web`. If the shown path is not the profile you installed into (Desktop builds may use a different profile or a different `DSH_HOME`), that is the cause — align `--profile` / `DSH_HOME` with it.
368
407
 
369
408
  ### 🌐 Behind a proxy? (GitHub login / sync)
370
409
 
package/README.zh-CN.md CHANGED
@@ -80,6 +80,8 @@ DSH 是你的 AI 助手工作台,里面存着你的各种设置:模型配置
80
80
  | ⏰ | **定时全量备份** | 按固定周期(6h / 12h / 24h / 7d)自动全量备份,一劳永逸,密钥永不包含 |
81
81
  | 🛒 | **配置市场** | 浏览并一键安装社区分享的配置——供应链警示 + 逐分区批准 |
82
82
  | 🗂️ | **配置档案 Profiles** | 保存多套配置(工作 / 个人),随时切换 |
83
+ | 🧩 | **本地插件随备份迁移** | `link:` / `file:` 安装的本地开发插件会被打包进备份,换机不再丢失 |
84
+ | 🗄️ | **保留策略可配(GFS 分层)** | 「最近 N 份 + 每月留 1 份 + 每年留 1 份」,默认值等价旧行为 |
83
85
  | 🤖 | **Agent 工具** | Agent 会话内直接备份 / 快照 / 恢复 / 同步 |
84
86
 
85
87
  ---
@@ -293,6 +295,10 @@ dsh-config-manager restore [--id <id>] [--dry-run]
293
295
  [--profile <name>] [--settings <path>]
294
296
  dsh-config-manager reinstall [--version <v>] [--yes] [--list]
295
297
  [--wipe-config] [--dry-run] # 一键重装 DSH 程序本体
298
+ dsh-config-manager verify [--id <文件|路径>] [--json] # 只读自检备份 ZIP
299
+ [--data-dir <dir>]
300
+ dsh-config-manager backup [--sections <a,b,c>] [--out <path>] # 离线文件级备份
301
+ [--dry-run] [--data-dir <dir>]
296
302
  ```
297
303
 
298
304
  **`reinstall` —— DSH 损坏时的救急重装。** 跨平台一键重装 `@deepseek-ai/dsh` 启动器(按操作系统自动选用正确命令:Windows 走 PowerShell、Unix 走 bash)。默认重装启动器 + 清全局残留缓存;交互式多选会询问是否勾选**危险**清理项(设置 / 插件 / 会话与凭据)——这些**默认不勾选**,且只要涉及删数据的动作,执行前都必须**二次确认输入 `YES`**。清空 `~/.dsh` 数据前会先做一份 `.reinstall-backup` 紧急备份(`snapshots/` 目录按设计绝不触碰)。
@@ -324,6 +330,41 @@ dsh-config-manager restore --id <snapshot-id> # 执行恢复(
324
330
 
325
331
  每次覆盖/删除前都会先把当前文件复制到 `<snapshotDir>/pre-restore/`,可人工反悔。任一动作失败则退出码为 1;报告如实列出 已还原 / 已卸载插件 / 需人工处理 / 失败 / 跳过。
326
332
 
333
+ **`verify` —— 三个月前那个备份现在还能不能用?** GUI 长在 DSH 里,DSH 起不来时它答不了这个问题,`verify` 能。它把备份 ZIP 从磁盘重读一遍,**一个字节都不写**,然后逐文件给出裁决:
334
+
335
+ | 裁决 | 含义 |
336
+ |---|---|
337
+ | `OK` | 结构合法,且每个条目都与 `integrity/checksums.json` 里的 SHA-256 一致 |
338
+ | `MISSING` | 文件不在(路径写错 / 已被删除) |
339
+ | `CORRUPT` | 损坏或被篡改——报错会**点名具体是哪个条目** |
340
+ | `UNSUPPORTED` | 备份本身有效,但本版本插件读不了(schema 过新,或整体加密容器需先解密) |
341
+ | `VERIFY_ERROR` | 自检自身失败(磁盘 IO);绝不降级成猜测结论 |
342
+
343
+ 不给参数就校验导出目录下**全部** `*.zip`;给文件名或路径就只校验那一个。**只要有一个不是 `OK` 退出码就是 `1`**,因此可直接在 CI / 定时任务里断言。`--json` 输出同样的机器可读结果。
344
+
345
+ ```bash
346
+ dsh-config-manager verify # 校验导出目录下的全部备份
347
+ dsh-config-manager verify --json # 机器可读(退出码语义不变)
348
+ dsh-config-manager verify my-backup.zip # 按文件名只校验一个
349
+ dsh-config-manager verify C:/backups/dsh-config.zip # 或按路径
350
+ ```
351
+
352
+ **`backup` —— DSH 挂了也能备份。** 它是 GUI 导出的离线版:把 `$DSH_HOME` 里**无需 DSH 运行时即可直读**的部分(skills、agent presets、agent instructions、插件自身配置)打成与 GUI 导出同结构的 ZIP(`manifest.json` + `integrity/checksums.json` + 分区目录),落盘后立即用与 `verify` 同一引擎对自己做一次自检——一个从不校验自己产物的备份命令,比没有更糟。
353
+
354
+ **凭据文件永不进入备份。** `.credentials.*`、`.env`、`*.pem` 等由显式黑名单排除;只遍历白名单目录(绝不对整个主目录递归);symlink 一律跳过而非跟随。
355
+
356
+ 结构化分区(设置 / UI / providers / 插件 / MCP / prompts / workspaces)**不会**被偷偷伪造:它们需要 DSH 服务层读值并脱敏,因此一律不写入、在 manifest 里标记为 `false`——计划输出会把它们列在「离线不可收集」下,让你清楚这份备份到底有什么、没有什么。DSH 健康时想要完整备份,请用 GUI 导出。
357
+
358
+ ```bash
359
+ dsh-config-manager backup --dry-run # 列出将打包哪些文件(零写入)
360
+ dsh-config-manager backup # 写入导出目录,随后自动自检
361
+ dsh-config-manager backup --out D:/rescue/config.zip # 指定输出(绝不覆盖既有文件)
362
+ dsh-config-manager backup --sections skills,self # 收窄范围
363
+ ```
364
+
365
+ `--sections` 可选值为 `skills,agentPresets,agentInstructions,self,pluginFiles`。其中 `pluginFiles` 与 GUI 侧一致属**默认关闭**:它原样复制第三方插件自有文件,而 `dsh-ssh.json` 里是明文主机密码——确认过内容之后再选它。
366
+
367
+
327
368
  **典型救急流程**(DSH 起不来时):① `dsh-config-manager reinstall` 先把启动器重装回来(必要时顺带清理),② `dsh web` 重新启动 DSH,③ 从仓库装回插件,④ 从远程仓库拉取快照(或执行 `dsh-config-manager restore`)把配置恢复回来。整个流程中 CLI 全程可用,与 DSH 是否健康无关。
328
369
 
329
370
  ### 🤖 Agent 可调用的模型工具
@@ -393,6 +434,7 @@ dsh-config-manager restore --id <snapshot-id> # 执行恢复(
393
434
  4. **历史会话默认不迁移**(v1 仅支持文件级复制)
394
435
  5. **加密备份**:密码丢失则无法解密(设计使然——请牢记密码)
395
436
  6. **快照恢复是离线的、诚实的**:离线引擎无法恢复的条目(快照无整文件备份时的 settings namespace / patch 行、存在 DSH storages 里的 workspace 记录)会如实列为跳过并指向在线回滚;凭据**值**绝不自动改写(只提示人工补录);无插件基线的旧快照只提示人工核对新增插件
437
+ 7. **本地源插件(`link:` / `file:`)随备份打包**:导出时执行 `npm pack` 把本地开发中的插件打成 tarball 一并备份,导入时解包到 `$DSH_HOME/dsh-config-manager/local-plugins/` 后按 `file:` 安装。因此:① 备份体积会随本地插件的体积增大(单插件超过 100 MB 会被跳过并告警,建议先发布到 registry / git 再备份);② 插件**源码**会进入备份(与「密钥永不进备份」不冲突——密钥仍被排除,这里进的是代码);③ 打包需要本机有可用的 `npm`,无 npm 时该插件退化为原行为(保留原 spec,换机后仍需手工安装)
396
438
 
397
439
  > 维护者与开发者:构建、测试、自动发布与完整技术说明见 [DEVELOPERS.md](DEVELOPERS.md)。
398
440
 
@@ -11,17 +11,21 @@ import path from 'node:path';
11
11
  import { sha256Hex } from '../utils/hashing.js';
12
12
  import { isReservedInternalRel, normalizePath } from '../utils/paths.js';
13
13
  import { msgOf, zhMsg } from '../core/messages.js';
14
+ import { linkWarnings, listFilesDetailed } from './link-report.js';
14
15
  export class FileCollectionAdapter {
15
16
  async export(ctx, _options) {
16
17
  const files = [];
17
18
  const warnings = [];
18
- let rels = [];
19
+ // issue #37:用「跟随 junction/符号链接」的遍历,并把跟随/跳过的链接写进告警——
20
+ // 此前链接目录及其全部内容被静默排除,备份仍报成功。
21
+ let listing = { paths: [], skippedLinks: [], followedLinks: 0, unreadableDirs: [] };
19
22
  try {
20
- rels = await ctx.fs.listRecursive(this.baseDir);
23
+ listing = await listFilesDetailed(ctx.fs, this.baseDir);
21
24
  }
22
25
  catch {
23
26
  // 目录不存在视为空
24
27
  }
28
+ const rels = listing.paths;
25
29
  for (const rel of rels) {
26
30
  const data = await ctx.fs.readFile(rel);
27
31
  const relPath = this.baseDir === '' ? rel : rel.replace(new RegExp(`^${escapeRegExp(this.baseDir)}[\\/]`), '');
@@ -29,6 +33,7 @@ export class FileCollectionAdapter {
29
33
  }
30
34
  if (rels.length === 0)
31
35
  warnings.push(msgOf(ctx)('adapter.dirEmpty', { type: this.displayName }));
36
+ warnings.push(...linkWarnings(msgOf(ctx), this.displayName, listing));
32
37
  return {
33
38
  sectionId: this.id,
34
39
  data: { version: 1, files },
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import type { ConfigAdapter } from '../core/types.ts';
11
11
  import { type NamespaceProvider } from './settings.ts';
12
+ import { type LocalPluginPackHook } from './plugins.ts';
12
13
  import { type CredentialRefsProvider } from './credentials.ts';
13
14
  export interface AdapterRegistryOptions {
14
15
  /** settings namespace 清单(宿主从 settings.yaml 顶层 key 解析注入;缺省 [] → settings/ui/credentials 为空) */
@@ -23,6 +24,11 @@ export interface AdapterRegistryOptions {
23
24
  includeSessions?: boolean;
24
25
  /** 插件自身包名:导出 plugins 分区时不列自己(避免自引用;缺省 dsh-config-manager) */
25
26
  selfPluginName?: string;
27
+ /**
28
+ * T1:本地源(link:/file:)插件打包钩子。注入后,导出 plugins 分区时会为这类插件
29
+ * 执行 `npm pack` 并把 tarball 随备份带走;不注入 = 保持改造前行为(本地插件换机丢失)。
30
+ */
31
+ localPluginPack?: LocalPluginPackHook;
26
32
  /**
27
33
  * self 分区:插件自身配置目录(相对 ~/.dsh 根,如 'dsh-config-manager')。
28
34
  * 缺省 'dsh-config-manager';传入 '' 表示不挂载(宿主自定义 dataDir 在 homeDir 外时)。
@@ -37,6 +43,7 @@ export { SettingsAdapter } from './settings.ts';
37
43
  export { UiAdapter, isUiNamespace, KNOWN_UI_NAMESPACE_PREFIXES, UI_MIGRATION_NOTES } from './ui.ts';
38
44
  export { ProvidersAdapter, DEFAULT_PROVIDER_NAMESPACES, type ProviderExportEntry, type ProviderExportSection } from './providers.ts';
39
45
  export { PluginsAdapter, USER_PATCH_FILE } from './plugins.ts';
46
+ export type { LocalPluginPackHook } from './plugins.ts';
40
47
  export { McpAdapter, extractMcpServers, buildMcpPatchLine, type McpExportEntry, type McpExportSection } from './mcp.ts';
41
48
  export { PromptsAdapter, extractPrompts, mergePromptIntoLine, buildPromptLine, type PromptExportEntry, type PromptsExportSection } from './prompts.ts';
42
49
  export { SkillsAdapter } from './skills.ts';
@@ -19,7 +19,7 @@ export function createAdapters(options = {}) {
19
19
  new SettingsAdapter(namespaces),
20
20
  new UiAdapter(namespaces),
21
21
  new ProvidersAdapter(),
22
- new PluginsAdapter(options.selfPluginName),
22
+ new PluginsAdapter(options.selfPluginName, options.localPluginPack),
23
23
  new McpAdapter(),
24
24
  new PromptsAdapter(),
25
25
  new SkillsAdapter(),
@@ -0,0 +1,16 @@
1
+ /**
2
+ * 文件类分区的「链接目录」报告(issue #37)。
3
+ *
4
+ * 背景:备份静默跳过 junction/符号链接,用户拿到的仍是「成功」,却少了整块内容。
5
+ * 修复分两步:① `utils/recursive-walk.ts` 跟随链接收集内容;② 本模块把「跟随了什么、
6
+ * 跳过了什么、为什么跳过」变成备份报告里可见的告警 —— 缺了 ②,用户依然无从察觉缺失。
7
+ *
8
+ * 纯函数(除 listFilesDetailed 的 IO 委托外),两个 adapter 共用,避免文案再次漂移。
9
+ */
10
+ import type { FileSystemFacade } from '../core/types.ts';
11
+ import type { MsgFunc } from '../core/messages.ts';
12
+ import type { RecursiveListing } from '../utils/recursive-walk.ts';
13
+ /** 取遍历结果:宿主未实现 listRecursiveDetailed(旧版/测试 mock)时回退,行为与旧版一致。 */
14
+ export declare function listFilesDetailed(fs: FileSystemFacade, dir: string): Promise<RecursiveListing>;
15
+ /** 把遍历结果转成告警行(无链接时返回空数组 —— 不制造噪音)。 */
16
+ export declare function linkWarnings(msg: MsgFunc, type: string, listing: RecursiveListing): string[];
@@ -0,0 +1,47 @@
1
+ /** 跳过原因 → 消息 key(与 SkippedLink.reason 一一对应,穷尽映射) */
2
+ const REASON_KEY = {
3
+ loop: 'adapter.linkReason.loop',
4
+ 'outside-home': 'adapter.linkReason.outsideHome',
5
+ broken: 'adapter.linkReason.broken',
6
+ unreadable: 'adapter.linkReason.unreadable',
7
+ 'too-deep': 'adapter.linkReason.tooDeep',
8
+ };
9
+ /** 单条原因下最多列出的路径数(其余折叠为计数,避免把报告撑爆) */
10
+ const MAX_PATHS_PER_REASON = 5;
11
+ /** 取遍历结果:宿主未实现 listRecursiveDetailed(旧版/测试 mock)时回退,行为与旧版一致。 */
12
+ export async function listFilesDetailed(fs, dir) {
13
+ if (fs.listRecursiveDetailed !== undefined)
14
+ return fs.listRecursiveDetailed(dir);
15
+ return { paths: await fs.listRecursive(dir), skippedLinks: [], followedLinks: 0, unreadableDirs: [] };
16
+ }
17
+ /** 把遍历结果转成告警行(无链接时返回空数组 —— 不制造噪音)。 */
18
+ export function linkWarnings(msg, type, listing) {
19
+ const out = [];
20
+ if (listing.followedLinks > 0) {
21
+ out.push(msg('adapter.linksFollowed', { type, count: String(listing.followedLinks) }));
22
+ }
23
+ if (listing.skippedLinks.length > 0) {
24
+ const byReason = new Map();
25
+ for (const s of listing.skippedLinks) {
26
+ const arr = byReason.get(s.reason) ?? [];
27
+ arr.push(s.path);
28
+ byReason.set(s.reason, arr);
29
+ }
30
+ const detail = [...byReason.entries()].map(([reason, paths]) => {
31
+ const shown = paths.slice(0, MAX_PATHS_PER_REASON).join(', ');
32
+ const more = paths.length > MAX_PATHS_PER_REASON ? ` (+ ${paths.length - MAX_PATHS_PER_REASON})` : '';
33
+ return `${msg(REASON_KEY[reason])} ${paths.length} 个: ${shown}${more}`;
34
+ }).join(';');
35
+ out.push(msg('adapter.linksSkipped', { type, count: String(listing.skippedLinks.length), detail }));
36
+ }
37
+ if (listing.unreadableDirs.length > 0) {
38
+ const shown = listing.unreadableDirs.slice(0, MAX_PATHS_PER_REASON).join(', ');
39
+ const more = listing.unreadableDirs.length > MAX_PATHS_PER_REASON
40
+ ? ` (+ ${listing.unreadableDirs.length - MAX_PATHS_PER_REASON})` : '';
41
+ out.push(msg('adapter.dirsUnreadable', {
42
+ type, count: String(listing.unreadableDirs.length), detail: `${shown}${more}`,
43
+ }));
44
+ }
45
+ return out;
46
+ }
47
+ //# sourceMappingURL=link-report.js.map
@@ -7,7 +7,8 @@
7
7
  * 导入时按同一相对路径写回原位置。默认关闭(defaultIncluded=false,用户显式勾选才导出)。
8
8
  */
9
9
  import { sha256Hex } from '../utils/hashing.js';
10
- import { zhMsg } from '../core/messages.js';
10
+ import { msgOf, zhMsg } from '../core/messages.js';
11
+ import { linkWarnings, listFilesDetailed } from './link-report.js';
11
12
  import { isPathSafe, isReservedInternalRel } from '../utils/paths.js';
12
13
  export const DEFAULT_PLUGIN_FILE_WHITELIST = ['dsh-ssh.json', 'pet.json'];
13
14
  export class PluginFilesAdapter {
@@ -40,14 +41,16 @@ export class PluginFilesAdapter {
40
41
  }
41
42
  }
42
43
  // 2) 约定配置目录递归收集(相对 ~/.dsh 根的完整路径;与白名单文件去重)
44
+ // issue #37:与 skills 等同一条遍历(跟随 junction/符号链接 + 跳过留痕)
45
+ let listing = { paths: [], skippedLinks: [], followedLinks: 0, unreadableDirs: [] };
43
46
  if (this.collectDir !== undefined) {
44
- let rels = [];
45
47
  try {
46
- rels = await ctx.fs.listRecursive(this.collectDir);
48
+ listing = await listFilesDetailed(ctx.fs, this.collectDir);
47
49
  }
48
50
  catch {
49
51
  // 目录不存在视为空
50
52
  }
53
+ const rels = listing.paths;
51
54
  for (const rel of rels) {
52
55
  if (seen.has(rel))
53
56
  continue;
@@ -64,7 +67,7 @@ export class PluginFilesAdapter {
64
67
  sectionId: 'pluginFiles',
65
68
  data: { version: 1, files },
66
69
  counts: { files: files.length },
67
- warnings: [],
70
+ warnings: linkWarnings(msgOf(ctx), this.displayName, listing),
68
71
  };
69
72
  }
70
73
  async analyzeImport(data, ctx) {
@@ -1,9 +1,23 @@
1
+ import type { PackLocalPluginsResult } from '../core/local-plugin-pack.ts';
1
2
  import type { MsgFunc } from '../core/messages.ts';
2
- import type { PluginsSection } from '../schema/types.ts';
3
+ import type { PluginEntry, PluginsSection } from '../schema/types.ts';
3
4
  import type { ApplyResult, ConfigAdapter, ExportOptions, ExportSection, HostContext, ImportContext, PlanItem, ValidationResult } from '../core/types.ts';
4
5
  export declare const USER_PATCH_FILE = "cordis.patch.yml";
6
+ /**
7
+ * 本地源插件打包钩子(由宿主注入,见 src/index.ts createAdapters)。
8
+ * 返回打包结果(含字节与重写后的 spec);不注入 = 不做本地源打包(保持旧行为)。
9
+ */
10
+ export type LocalPluginPackHook = (plugins: PluginEntry[], ctx: HostContext) => Promise<PackLocalPluginsResult>;
11
+ /** 本地插件 tarball 解包到 homeDir 内的固定缓存目录(相对 $DSH_HOME)。 */
12
+ export declare const LOCAL_TARBALL_CACHE_REL = "dsh-config-manager/local-plugins";
13
+ /** 归档内相对路径 → 缓存目录下的安全文件名(拒绝任何路径穿越)。 */
14
+ export declare function localTarballCacheName(relativePath: string): string;
15
+ /** profile 目录相对 $DSH_HOME 的路径(patches/ 与 pnpm-workspace.yaml 都挂在这里)。 */
16
+ export declare const PNPM_PROFILE_DIR: (profile: string | undefined) => string;
5
17
  /** pnpm-workspace.yaml 相对 $DSH_HOME 的路径(plugins 分区内按「插件安装配置」管理)。 */
6
18
  export declare const PNPM_WORKSPACE_REL: (profile: string | undefined) => string;
19
+ /** 单个 patch 文件随备份迁移的体积上限(issue #35;patch 是纯文本,超过说明放错了东西)。 */
20
+ export declare const MAX_PATCH_FILE_BYTES: number;
7
21
  export declare class PluginsAdapter implements ConfigAdapter<PluginsSection> {
8
22
  readonly id: 'plugins';
9
23
  readonly displayName = "Plugins";
@@ -11,8 +25,16 @@ export declare class PluginsAdapter implements ConfigAdapter<PluginsSection> {
11
25
  readonly portability: 'portable';
12
26
  /** 插件自身包名:导出 plugins 分区时不列自己(避免备份里出现「当前正在生成备份的插件」的自引用条目) */
13
27
  private readonly selfName;
14
- constructor(selfName?: string);
28
+ /** T1:本地源(link:/file:)插件打包钩子;未注入 = 不打包(保持改造前行为) */
29
+ private readonly localPack;
30
+ constructor(selfName?: string, localPack?: LocalPluginPackHook);
15
31
  export(ctx: HostContext, _options: ExportOptions): Promise<ExportSection<PluginsSection>>;
32
+ /**
33
+ * 导入后目标机**可能**存在的 patch 文件集合(相对 profile 目录)——issue #35。
34
+ * = 备份携带的 patchFiles ∪ 目标机本来就有的文件。只用于判定「声明能否被满足」,
35
+ * 不做任何写入(写入在 applyItem 内、且先于 pnpm-workspace.yaml)。
36
+ */
37
+ private patchAvailability;
16
38
  analyzeImport(data: PluginsSection, ctx: ImportContext): Promise<PlanItem[]>;
17
39
  applyItem(item: PlanItem, ctx: ImportContext): Promise<ApplyResult>;
18
40
  validate(data: PluginsSection, msg?: MsgFunc): Promise<ValidationResult>;