dsh-config-manager 0.1.57 → 0.1.59

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 (257) hide show
  1. package/README.md +38 -0
  2. package/README.zh-CN.md +42 -0
  3. package/lib/adapters/index.d.ts +7 -0
  4. package/lib/adapters/index.js +1 -1
  5. package/lib/adapters/plugins.d.ts +14 -2
  6. package/lib/adapters/plugins.js +138 -5
  7. package/lib/cli/index.d.ts +11 -1
  8. package/lib/cli/index.js +325 -4
  9. package/lib/client.d.ts +86 -1
  10. package/lib/client.js +6752 -6061
  11. package/lib/core/analyzer.d.ts +17 -1
  12. package/lib/core/analyzer.js +123 -23
  13. package/lib/core/backup-plan.d.ts +69 -0
  14. package/lib/core/backup-plan.js +269 -0
  15. package/lib/core/backup-verify.d.ts +24 -0
  16. package/lib/core/backup-verify.js +271 -0
  17. package/lib/core/backup.d.ts +45 -0
  18. package/lib/core/backup.js +16 -2
  19. package/lib/core/exporter.d.ts +9 -0
  20. package/lib/core/exporter.js +110 -2
  21. package/lib/core/local-plugin-host.d.ts +35 -0
  22. package/lib/core/local-plugin-host.js +120 -0
  23. package/lib/core/local-plugin-pack.d.ts +135 -0
  24. package/lib/core/local-plugin-pack.js +278 -0
  25. package/lib/core/messages.d.ts +10 -0
  26. package/lib/core/messages.js +20 -0
  27. package/lib/core/model-tools.d.ts +8 -1
  28. package/lib/core/model-tools.js +2 -0
  29. package/lib/core/types.d.ts +24 -0
  30. package/lib/index.d.ts +1 -1
  31. package/lib/index.js +46 -3
  32. package/lib/market/prepare.js +13 -0
  33. package/lib/market/security.js +17 -0
  34. package/lib/schema/index.d.ts +24 -0
  35. package/lib/schema/index.js +26 -0
  36. package/lib/schema/types.d.ts +33 -0
  37. package/lib/security/encryption.d.ts +0 -5
  38. package/lib/security/encryption.js +4 -11
  39. package/lib/sync/backup-files.d.ts +14 -0
  40. package/lib/sync/backup-files.js +37 -0
  41. package/lib/sync/backup-schedule-config.d.ts +13 -0
  42. package/lib/sync/backup-schedule-config.js +26 -0
  43. package/lib/sync/backup-scheduler.d.ts +16 -3
  44. package/lib/sync/backup-scheduler.js +18 -5
  45. package/lib/sync/retention-policy.d.ts +143 -0
  46. package/lib/sync/retention-policy.js +237 -0
  47. package/lib/ui/backup-schedule.d.ts +30 -2
  48. package/lib/ui/backup-schedule.js +33 -0
  49. package/lib/ui/history-model.d.ts +26 -0
  50. package/lib/ui/history-model.js +53 -0
  51. package/lib/ui/migration-consult-view.d.ts +34 -0
  52. package/lib/ui/migration-consult-view.js +50 -0
  53. package/lib/ui/test-helpers.js +2 -0
  54. package/lib/utils/bundle-scan.d.ts +133 -0
  55. package/lib/utils/bundle-scan.js +297 -0
  56. package/lib/utils/env-lock.d.ts +13 -0
  57. package/lib/utils/env-lock.js +33 -4
  58. package/package.json +57 -6
  59. package/src/adapters/index.ts +8 -2
  60. package/src/adapters/plugins.ts +159 -6
  61. package/src/cli/index.ts +357 -6
  62. package/src/client/about/AboutPanel.tsx +1 -1
  63. package/src/client/bundle-selfcontained.test.ts +85 -0
  64. package/src/client/common/SectionComposition.tsx +36 -0
  65. package/src/client/config-manager.module.css +306 -13
  66. package/src/client/consult/ConsultCard.tsx +48 -34
  67. package/src/client/export/ExportView.tsx +33 -27
  68. package/src/client/history/HistoryPanel.tsx +13 -5
  69. package/src/client/import/ConflictList.tsx +51 -2
  70. package/src/client/import/ImportWizardView.tsx +1 -1
  71. package/src/client/import/PathMappingForm.tsx +3 -2
  72. package/src/client/locales.ts +32 -0
  73. package/src/client/market/MarketPanel.tsx +13 -7
  74. package/src/client/market/MyConfigsView.tsx +9 -5
  75. package/src/client/market/market-view.test.ts +2 -0
  76. package/src/client/overview/OverviewPanel.tsx +9 -10
  77. package/src/client/run-store.test.ts +48 -13
  78. package/src/client/snapshots/SnapshotsPanel.tsx +198 -49
  79. package/src/client/sync/SyncHistoryView.tsx +74 -32
  80. package/src/client/sync/SyncSettingsView.tsx +38 -19
  81. package/src/client/sync/history-model.test.ts +104 -2
  82. package/src/client/sync/history-model.ts +79 -0
  83. package/src/client/sync/sync-locales.ts +14 -0
  84. package/src/core/analyzer.ts +152 -25
  85. package/src/core/backup-plan.ts +324 -0
  86. package/src/core/backup-verify.ts +306 -0
  87. package/src/core/backup.ts +58 -1
  88. package/src/core/exporter.ts +110 -2
  89. package/src/core/local-plugin-host.ts +152 -0
  90. package/src/core/local-plugin-pack.test.ts +359 -0
  91. package/src/core/local-plugin-pack.ts +363 -0
  92. package/src/core/messages.ts +20 -0
  93. package/src/core/model-tools.test.ts +92 -2
  94. package/src/core/model-tools.ts +10 -1
  95. package/src/core/types.ts +21 -0
  96. package/src/index.ts +55 -4
  97. package/src/market/prepare.test.ts +60 -0
  98. package/src/market/prepare.ts +15 -0
  99. package/src/market/security.ts +17 -0
  100. package/src/schema/index.ts +71 -0
  101. package/src/schema/manifest-schema.test.ts +519 -0
  102. package/src/schema/types.ts +35 -0
  103. package/src/security/encryption.ts +4 -12
  104. package/src/security/security.test.ts +3 -6
  105. package/src/sync/backup-files.ts +42 -0
  106. package/src/sync/backup-schedule-config.ts +32 -0
  107. package/src/sync/backup-scheduler.test.ts +228 -2
  108. package/src/sync/backup-scheduler.ts +34 -9
  109. package/src/sync/retention-policy.test.ts +418 -0
  110. package/src/sync/retention-policy.ts +286 -0
  111. package/src/sync/sync-session.test.ts +2 -0
  112. package/src/ui/backup-inspect.test.ts +2 -0
  113. package/src/ui/backup-schedule.test.ts +75 -0
  114. package/src/ui/backup-schedule.ts +43 -2
  115. package/src/ui/history-model.test.ts +76 -1
  116. package/src/ui/history-model.ts +59 -0
  117. package/src/ui/migration-consult-view.test.ts +69 -0
  118. package/src/ui/migration-consult-view.ts +59 -0
  119. package/src/ui/test-helpers.ts +3 -1
  120. package/src/utils/bundle-scan.test.ts +187 -0
  121. package/src/utils/bundle-scan.ts +335 -0
  122. package/src/utils/env-lock.test.ts +78 -0
  123. package/src/utils/env-lock.ts +32 -4
  124. package/lib/adapters/agent-instructions.js.map +0 -1
  125. package/lib/adapters/agent-presets.js.map +0 -1
  126. package/lib/adapters/credentials.js.map +0 -1
  127. package/lib/adapters/file-collection.js.map +0 -1
  128. package/lib/adapters/index.js.map +0 -1
  129. package/lib/adapters/mcp.js.map +0 -1
  130. package/lib/adapters/plugin-files.js.map +0 -1
  131. package/lib/adapters/plugins.js.map +0 -1
  132. package/lib/adapters/prompts.js.map +0 -1
  133. package/lib/adapters/providers.js.map +0 -1
  134. package/lib/adapters/self.js.map +0 -1
  135. package/lib/adapters/sessions.js.map +0 -1
  136. package/lib/adapters/settings.js.map +0 -1
  137. package/lib/adapters/skills.js.map +0 -1
  138. package/lib/adapters/test-helpers.js.map +0 -1
  139. package/lib/adapters/ui.js.map +0 -1
  140. package/lib/adapters/workspaces.js.map +0 -1
  141. package/lib/cli/index.js.map +0 -1
  142. package/lib/client.js.map +0 -1
  143. package/lib/core/analyzer.js.map +0 -1
  144. package/lib/core/backup.js.map +0 -1
  145. package/lib/core/cache-cleaner.js.map +0 -1
  146. package/lib/core/consult-source.js.map +0 -1
  147. package/lib/core/exporter.js.map +0 -1
  148. package/lib/core/ghost-sweep.js.map +0 -1
  149. package/lib/core/importer.js.map +0 -1
  150. package/lib/core/index.js.map +0 -1
  151. package/lib/core/journal.js.map +0 -1
  152. package/lib/core/messages.js.map +0 -1
  153. package/lib/core/migration-consult.js.map +0 -1
  154. package/lib/core/migration-history.js.map +0 -1
  155. package/lib/core/model-tools.js.map +0 -1
  156. package/lib/core/msg-types.js.map +0 -1
  157. package/lib/core/phase3-child-crash.js.map +0 -1
  158. package/lib/core/phase3-host.js.map +0 -1
  159. package/lib/core/phase3-prod-child.js.map +0 -1
  160. package/lib/core/phase4-crash-child.js.map +0 -1
  161. package/lib/core/plugin-cli.js.map +0 -1
  162. package/lib/core/reconcile.js.map +0 -1
  163. package/lib/core/recovery-orchestrator.js.map +0 -1
  164. package/lib/core/reinstall.js.map +0 -1
  165. package/lib/core/restore.js.map +0 -1
  166. package/lib/core/rollback.js.map +0 -1
  167. package/lib/core/run-registry.js.map +0 -1
  168. package/lib/core/startup-barrier.js.map +0 -1
  169. package/lib/core/transaction-coordinator.js.map +0 -1
  170. package/lib/core/types.js.map +0 -1
  171. package/lib/core/validator.js.map +0 -1
  172. package/lib/core/verify-recovery.js.map +0 -1
  173. package/lib/index.js.map +0 -1
  174. package/lib/market/builtin.js.map +0 -1
  175. package/lib/market/git-file-writer.js.map +0 -1
  176. package/lib/market/github-repos.js.map +0 -1
  177. package/lib/market/index-parser.js.map +0 -1
  178. package/lib/market/market-config.js.map +0 -1
  179. package/lib/market/my-repo.js.map +0 -1
  180. package/lib/market/prepare.js.map +0 -1
  181. package/lib/market/reader.js.map +0 -1
  182. package/lib/market/repo-url.js.map +0 -1
  183. package/lib/market/security.js.map +0 -1
  184. package/lib/market/star-cache.js.map +0 -1
  185. package/lib/market/types.js.map +0 -1
  186. package/lib/market/upstream.js.map +0 -1
  187. package/lib/market/url.js.map +0 -1
  188. package/lib/market/view.js.map +0 -1
  189. package/lib/migrations/index.js.map +0 -1
  190. package/lib/migrations/v1-to-v2.js.map +0 -1
  191. package/lib/profiles/index.js.map +0 -1
  192. package/lib/profiles/profile-manager.js.map +0 -1
  193. package/lib/schema/config.js.map +0 -1
  194. package/lib/schema/manifest.js.map +0 -1
  195. package/lib/schema/tombstones.js.map +0 -1
  196. package/lib/schema/types.js.map +0 -1
  197. package/lib/schema/versions.js.map +0 -1
  198. package/lib/security/encryption.js.map +0 -1
  199. package/lib/security/index.js.map +0 -1
  200. package/lib/security/integrity.js.map +0 -1
  201. package/lib/security/redaction.js.map +0 -1
  202. package/lib/security/secret-scanner.js.map +0 -1
  203. package/lib/security/vault.js.map +0 -1
  204. package/lib/security/zip-security.js.map +0 -1
  205. package/lib/sync/ancestor.js.map +0 -1
  206. package/lib/sync/autosync-config.js.map +0 -1
  207. package/lib/sync/autosync-scheduler.js.map +0 -1
  208. package/lib/sync/backup-files.js.map +0 -1
  209. package/lib/sync/backup-schedule-config.js.map +0 -1
  210. package/lib/sync/backup-scheduler.js.map +0 -1
  211. package/lib/sync/fs.js.map +0 -1
  212. package/lib/sync/git/git-transport.js.map +0 -1
  213. package/lib/sync/github-auth.js.map +0 -1
  214. package/lib/sync/layout.js.map +0 -1
  215. package/lib/sync/merge.js.map +0 -1
  216. package/lib/sync/review-queue.js.map +0 -1
  217. package/lib/sync/risk.js.map +0 -1
  218. package/lib/sync/snapshot-crypto.js.map +0 -1
  219. package/lib/sync/snapshot-json.js.map +0 -1
  220. package/lib/sync/sync-config.js.map +0 -1
  221. package/lib/sync/sync-engine.js.map +0 -1
  222. package/lib/sync/sync-history.js.map +0 -1
  223. package/lib/sync/sync-selection.js.map +0 -1
  224. package/lib/sync/sync-session.js.map +0 -1
  225. package/lib/sync/sync-state.js.map +0 -1
  226. package/lib/sync/transport.js.map +0 -1
  227. package/lib/sync/ui-prefs.js.map +0 -1
  228. package/lib/sync/webdav/webdav-transport.js.map +0 -1
  229. package/lib/ui/backup-inspect.js.map +0 -1
  230. package/lib/ui/backup-schedule.js.map +0 -1
  231. package/lib/ui/conflict-view.js.map +0 -1
  232. package/lib/ui/errors.js.map +0 -1
  233. package/lib/ui/export-flow.js.map +0 -1
  234. package/lib/ui/flow.js.map +0 -1
  235. package/lib/ui/history-model.js.map +0 -1
  236. package/lib/ui/i18n.js.map +0 -1
  237. package/lib/ui/import-stepper.js.map +0 -1
  238. package/lib/ui/import-wizard.js.map +0 -1
  239. package/lib/ui/migration-consult-view.js.map +0 -1
  240. package/lib/ui/next-steps.js.map +0 -1
  241. package/lib/ui/overview-view.js.map +0 -1
  242. package/lib/ui/path-mapping.js.map +0 -1
  243. package/lib/ui/profiles-view.js.map +0 -1
  244. package/lib/ui/progress.js.map +0 -1
  245. package/lib/ui/release-notes-prompt.js.map +0 -1
  246. package/lib/ui/report.js.map +0 -1
  247. package/lib/ui/star-prompt.js.map +0 -1
  248. package/lib/ui/test-helpers.js.map +0 -1
  249. package/lib/ui/types.js.map +0 -1
  250. package/lib/utils/atomic-write.js.map +0 -1
  251. package/lib/utils/env-lock.js.map +0 -1
  252. package/lib/utils/hashing.js.map +0 -1
  253. package/lib/utils/json.js.map +0 -1
  254. package/lib/utils/logger.js.map +0 -1
  255. package/lib/utils/paths.js.map +0 -1
  256. package/lib/utils/proxy.js.map +0 -1
  257. 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).
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
 
@@ -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(),
@@ -1,7 +1,17 @@
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;
5
15
  /** pnpm-workspace.yaml 相对 $DSH_HOME 的路径(plugins 分区内按「插件安装配置」管理)。 */
6
16
  export declare const PNPM_WORKSPACE_REL: (profile: string | undefined) => string;
7
17
  export declare class PluginsAdapter implements ConfigAdapter<PluginsSection> {
@@ -11,7 +21,9 @@ export declare class PluginsAdapter implements ConfigAdapter<PluginsSection> {
11
21
  readonly portability: 'portable';
12
22
  /** 插件自身包名:导出 plugins 分区时不列自己(避免备份里出现「当前正在生成备份的插件」的自引用条目) */
13
23
  private readonly selfName;
14
- constructor(selfName?: string);
24
+ /** T1:本地源(link:/file:)插件打包钩子;未注入 = 不打包(保持改造前行为) */
25
+ private readonly localPack;
26
+ constructor(selfName?: string, localPack?: LocalPluginPackHook);
15
27
  export(ctx: HostContext, _options: ExportOptions): Promise<ExportSection<PluginsSection>>;
16
28
  analyzeImport(data: PluginsSection, ctx: ImportContext): Promise<PlanItem[]>;
17
29
  applyItem(item: PlanItem, ctx: ImportContext): Promise<ApplyResult>;
@@ -4,11 +4,66 @@
4
4
  *
5
5
  * 安全不变量:绝不打包插件二进制;导入走 DSH 官方机制(dsh plugin CLI → needsRestart 提示)。
6
6
  * patch 行导入(用户自定义行:启用/禁用/插入)写回 cordis.patch.yml,同样 needsRestart。
7
+ *
8
+ * T1(本地源插件迁移):`link:` / `file:` 来源的插件指向本机路径,换机后必然不可达
9
+ * (曾导致插件被静默丢失)。导出时经注入的 `localPack` 执行 `npm pack`,把 tarball 作为
10
+ * 文件条目随分区进入 ZIP(落在 `plugin-files/local-plugins/` 前缀下,复用既有文件类分区通道,
11
+ * **不新增分区 id**);导入时把 spec 重写为 `file:<解包后的绝对路径>` 再交给官方安装通道。
7
12
  */
8
13
  import { isDeepStrictEqual } from 'node:util';
9
14
  import { installSpecFor, resolveProfileNameFromArgv } from '../core/plugin-cli.js';
15
+ import { isLocalPluginSpec, isPackedLocalSpec, LOCAL_PLUGIN_DIR } from '../core/local-plugin-pack.js';
10
16
  import { msgOf, zhMsg } from '../core/messages.js';
11
17
  export const USER_PATCH_FILE = 'cordis.patch.yml';
18
+ /** tarball 字节 → base64(零依赖,避免 Buffer 在浏览器侧类型问题) */
19
+ function bytesToBase64(data) {
20
+ if (typeof Buffer !== 'undefined')
21
+ return Buffer.from(data).toString('base64');
22
+ let binary = '';
23
+ for (const b of data)
24
+ binary += String.fromCharCode(b);
25
+ return btoa(binary);
26
+ }
27
+ /** base64 → 字节(导入端解包用) */
28
+ function base64ToBytes(b64) {
29
+ if (typeof Buffer !== 'undefined')
30
+ return new Uint8Array(Buffer.from(b64, 'base64'));
31
+ const binary = atob(b64);
32
+ const out = new Uint8Array(binary.length);
33
+ for (let i = 0; i < binary.length; i += 1)
34
+ out[i] = binary.charCodeAt(i);
35
+ return out;
36
+ }
37
+ /** 本地插件 tarball 解包到 homeDir 内的固定缓存目录(相对 $DSH_HOME)。 */
38
+ export const LOCAL_TARBALL_CACHE_REL = 'dsh-config-manager/local-plugins';
39
+ /** 按包名取出备份内的 tarball 条目(不存在返回 undefined)。 */
40
+ function tarballOf(data, pkgName) {
41
+ return data.localTarballs?.find((t) => t.packageName === pkgName);
42
+ }
43
+ /** 归档内相对路径 → 缓存目录下的安全文件名(拒绝任何路径穿越)。 */
44
+ export function localTarballCacheName(relativePath) {
45
+ const base = relativePath.split('/').pop() ?? '';
46
+ if (base === '' || base.includes('\\') || base === '.' || base === '..') {
47
+ throw new Error(`非法 tarball 相对路径: ${relativePath}`);
48
+ }
49
+ return base;
50
+ }
51
+ /**
52
+ * 把备份内的 tarball 写入 `$DSH_HOME/dsh-config-manager/local-plugins/<name>`,
53
+ * 返回**绝对路径**(供 `file:` spec 使用)。
54
+ *
55
+ * 为什么写进 $DSH_HOME 内:`ctx.target.fs` 是「限定在 home 根内」的门面
56
+ * (`DshFileSystemFacade.abs()` 对根外路径抛 fsPathEscape),写 homeDir 外会被拒。
57
+ * 该目录同时落在既有保留区内(`dsh-config-manager/`),随 self 分区语义一致。
58
+ */
59
+ async function writeLocalTarball(ctx, tarball) {
60
+ const name = localTarballCacheName(tarball.relativePath);
61
+ const rel = `${LOCAL_TARBALL_CACHE_REL}/${name}`;
62
+ await ctx.target.fs.writeFile(rel, base64ToBytes(tarball.base64));
63
+ // `file:` spec 一律用正斜杠(pnpm 跨平台接受正斜杠;反斜杠在部分版本需转义)
64
+ const abs = `${ctx.target.homeDir.replace(/[\\/]+$/, '')}/${rel}`;
65
+ return abs.replace(/\\/g, '/');
66
+ }
12
67
  /** pnpm-workspace.yaml 相对 $DSH_HOME 的路径(plugins 分区内按「插件安装配置」管理)。 */
13
68
  export const PNPM_WORKSPACE_REL = (profile) => `profiles/${profile !== undefined && profile !== '' ? profile : 'web'}/pnpm-workspace.yaml`;
14
69
  /** patch 行 raw 是否由其他 adapter 管理(mcp-client 行 / systemPrompt / planMode 行)。
@@ -50,8 +105,11 @@ export class PluginsAdapter {
50
105
  portability = 'portable';
51
106
  /** 插件自身包名:导出 plugins 分区时不列自己(避免备份里出现「当前正在生成备份的插件」的自引用条目) */
52
107
  selfName;
53
- constructor(selfName = 'dsh-config-manager') {
108
+ /** T1:本地源(link:/file:)插件打包钩子;未注入 = 不打包(保持改造前行为) */
109
+ localPack;
110
+ constructor(selfName = 'dsh-config-manager', localPack) {
54
111
  this.selfName = selfName;
112
+ this.localPack = localPack;
55
113
  }
56
114
  async export(ctx, _options) {
57
115
  const plugins = [];
@@ -97,10 +155,45 @@ export class PluginsAdapter {
97
155
  catch (err) {
98
156
  warnings.push(msgOf(ctx)('adapter.pnpmReadFailed', { reason: err instanceof Error ? err.message : String(err) }));
99
157
  }
158
+ // T1:本地源(link:/file:)插件打包。这些 spec 指向本机路径,换机后必然不可达
159
+ // (曾导致插件被静默丢失)。钩子由宿主注入;未注入 / 无本地源 / 单个失败一律不中断导出。
160
+ let localTarballs;
161
+ let effectivePlugins = plugins;
162
+ if (this.localPack !== undefined) {
163
+ try {
164
+ const packed = await this.localPack(plugins, ctx);
165
+ warnings.push(...packed.warnings);
166
+ if (packed.packed.length > 0) {
167
+ localTarballs = packed.packed.map((p) => ({
168
+ packageName: p.packageName,
169
+ version: p.version,
170
+ relativePath: p.relativePath,
171
+ base64: bytesToBase64(p.data),
172
+ }));
173
+ // 清单里的 spec 同步改写为可移植形式:即便导入端不拆 tarball,
174
+ // 也不会再把本机绝对路径写进目标 profile package.json。
175
+ const rewritten = packed.rewritten;
176
+ effectivePlugins = plugins.map((p) => rewritten[p.name] !== undefined ? { ...p, spec: rewritten[p.name] } : p);
177
+ }
178
+ }
179
+ catch (err) {
180
+ warnings.push(msgOf(ctx)('adapter.localPackFailed', { reason: err instanceof Error ? err.message : String(err) }));
181
+ }
182
+ }
100
183
  return {
101
184
  sectionId: 'plugins',
102
- data: { version: 1, plugins, patch, pnpmWorkspace },
103
- counts: { plugins: plugins.length, patchLines: patch.length },
185
+ data: {
186
+ version: 1,
187
+ plugins: effectivePlugins,
188
+ patch,
189
+ pnpmWorkspace,
190
+ ...(localTarballs !== undefined ? { localTarballs } : {}),
191
+ },
192
+ counts: {
193
+ plugins: effectivePlugins.length,
194
+ patchLines: patch.length,
195
+ ...(localTarballs !== undefined ? { localTarballs: localTarballs.length } : {}),
196
+ },
104
197
  warnings,
105
198
  };
106
199
  }
@@ -136,11 +229,14 @@ export class PluginsAdapter {
136
229
  for (const p of data.plugins) {
137
230
  const id = `plugin:${p.name}`;
138
231
  const tp = targetInstalled.find((t) => t.name === p.name);
232
+ // T1:该插件在备份内自带 tarball(本地源 link:/file:)→ 目标机无需原始路径即可安装。
233
+ const hasTarball = tarballOf(data, p.name) !== undefined;
234
+ const tarballHint = hasTarball ? msg('adapter.pluginLocalTarballHint') : undefined;
139
235
  if (!tp) {
140
236
  items.push({
141
237
  id, kind: 'Install', adapter: 'plugins',
142
238
  description: msg('adapter.pluginInstall', { name: p.name, version: p.version }),
143
- detail: p.isBundle ? msg('adapter.pluginBundleMember') : undefined,
239
+ detail: p.isBundle ? msg('adapter.pluginBundleMember') : tarballHint,
144
240
  severity: 'info',
145
241
  });
146
242
  }
@@ -218,7 +314,20 @@ export class PluginsAdapter {
218
314
  try {
219
315
  // 非 registry 来源(github:/file: 等)按来源 spec 安装,registry 包按裸包名装 npm 最新版
220
316
  const data = ctx.sections.get('plugins');
221
- const spec = data?.plugins.find((p) => p.name === name)?.spec;
317
+ let spec = data?.plugins.find((p) => p.name === name)?.spec;
318
+ // T1:备份内自带 tarball → 解包到 homeDir 内的本地插件缓存目录,把 spec 重写为绝对路径。
319
+ // 这是「换机后本地插件不再丢失」的关键:原始绝对路径在目标机不存在,解包出的 tgz 一定存在。
320
+ const tarball = data !== undefined ? tarballOf(data, name) : undefined;
321
+ if (tarball !== undefined) {
322
+ try {
323
+ const abs = await writeLocalTarball(ctx, tarball);
324
+ spec = `file:${abs}`;
325
+ }
326
+ catch (err) {
327
+ // 解包失败不阻断安装尝试:退回原 spec(可能失败,但会如实报错而非静默丢失)
328
+ ctx.onLog?.(`本地插件 ${name} 的 tarball 解包失败,回退原始 spec:${err instanceof Error ? err.message : String(err)}`);
329
+ }
330
+ }
222
331
  // 执行日志:记录实际将发起的子进程命令行(与宿主 DshPluginsFacade 的
223
332
  // dsh plugin --profile <p> add <spec> 一致);仅非敏感文本,渲染前 UI 再 redact 兜底
224
333
  ctx.onLog?.(`$ dsh plugin --profile ${ctx.target.profile ?? resolveProfileNameFromArgv()} add ${installSpecFor(name, spec)}`);
@@ -277,6 +386,30 @@ export class PluginsAdapter {
277
386
  if (data.pnpmWorkspace !== undefined && data.pnpmWorkspace !== null && typeof data.pnpmWorkspace !== 'string') {
278
387
  issues.push({ path: 'pnpmWorkspace', message: msg('adapter.validate.string', { subject: 'pnpmWorkspace' }), severity: 'error' });
279
388
  }
389
+ // T1:本地插件 tarball 载荷(缺省合法;存在时逐条校验形状与路径安全)
390
+ if (data.localTarballs !== undefined) {
391
+ if (!Array.isArray(data.localTarballs)) {
392
+ issues.push({ path: 'localTarballs', message: msg('adapter.validate.array', { subject: 'localTarballs' }), severity: 'error' });
393
+ }
394
+ else {
395
+ data.localTarballs.forEach((t, i) => {
396
+ if (t === null || typeof t !== 'object') {
397
+ issues.push({ path: `localTarballs[${i}]`, message: msg('adapter.validate.object', { subject: 'localTarballs[]' }), severity: 'error' });
398
+ return;
399
+ }
400
+ for (const field of ['packageName', 'version', 'relativePath', 'base64']) {
401
+ if (typeof t[field] !== 'string' || t[field] === '') {
402
+ issues.push({ path: `localTarballs[${i}].${field}`, message: msg('adapter.validate.string', { subject: field }), severity: 'error' });
403
+ }
404
+ }
405
+ // 相对路径必须落在 LOCAL_PLUGIN_DIR 之下且不含穿越段(防写 homeDir 外)
406
+ const rel = typeof t.relativePath === 'string' ? t.relativePath.replace(/\\/g, '/') : '';
407
+ if (rel !== '' && (!rel.startsWith(`${LOCAL_PLUGIN_DIR}/`) || rel.includes('..'))) {
408
+ issues.push({ path: `localTarballs[${i}].relativePath`, message: msg('adapter.validate.localTarballPath', { path: rel }), severity: 'error' });
409
+ }
410
+ });
411
+ }
412
+ }
280
413
  return { valid: issues.filter((i) => i.severity === 'error').length === 0, issues };
281
414
  }
282
415
  }
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { type SnapshotMeta } from '../core/restore.ts';
3
3
  import { type ReinstallItemId } from '../core/reinstall.ts';
4
- export type CliCommand = 'snapshots' | 'restore' | 'reinstall' | 'recover-stale-lock' | 'help';
4
+ export type CliCommand = 'snapshots' | 'restore' | 'reinstall' | 'recover-stale-lock' | 'verify' | 'backup' | 'help';
5
5
  export interface CliOptions {
6
6
  command: CliCommand;
7
7
  /** --data-dir:快照数据目录覆盖 */
@@ -22,6 +22,14 @@ export interface CliOptions {
22
22
  list: boolean;
23
23
  /** --wipe-config:reinstall 等价勾选全部数据类(settings/plugins/data) */
24
24
  wipeConfig: boolean;
25
+ /** --json:verify 输出机器可读结果(便于 CI / 定时任务断言) */
26
+ json: boolean;
27
+ /** --sections <a,b,c>:backup 要打包的离线分区白名单(缺省取默认分区) */
28
+ sections?: string;
29
+ /** --out <path>:backup 输出 ZIP 路径(缺省写入导出目录,自动去重不覆盖) */
30
+ out?: string;
31
+ /** verify 的位置参数(文件名或路径;与 --id 同义,最多一个) */
32
+ positionals: string[];
25
33
  }
26
34
  export type ParseResult = {
27
35
  ok: true;
@@ -65,3 +73,5 @@ export interface CliDeps {
65
73
  * reinstall → 交互多选 + 二次确认 + 跨平台执行。失败项存在时 exit 1,否则 0。
66
74
  */
67
75
  export declare function runCli(argv: readonly string[], io?: CliIo, env?: Record<string, string | undefined>, deps?: CliDeps): Promise<number>;
76
+ /** 导出目录:--data-dir 覆盖优先,缺省 $DSH_HOME/dsh-config-manager/exports */
77
+ export declare function resolveExportsDir(flag: string | undefined, env?: Record<string, string | undefined>): string;