dsh-config-manager 0.1.64 → 0.1.66

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 (176) hide show
  1. package/README.md +78 -12
  2. package/README.zh-CN.md +77 -11
  3. package/lib/adapters/plugins.js +3 -3
  4. package/lib/cli/index.d.ts +11 -0
  5. package/lib/cli/index.js +23 -9
  6. package/lib/client.d.ts +208 -155
  7. package/lib/client.js +5342 -3060
  8. package/lib/core/analyzer.d.ts +4 -0
  9. package/lib/core/analyzer.js +8 -22
  10. package/lib/core/backup.d.ts +2 -2
  11. package/lib/core/boot-paths.d.ts +14 -0
  12. package/lib/core/boot-paths.js +28 -0
  13. package/lib/core/boot-rescue.d.ts +21 -0
  14. package/lib/core/boot-rescue.js +57 -1
  15. package/lib/core/boot-safety.js +2 -2
  16. package/lib/core/conflict-decisions.d.ts +16 -0
  17. package/lib/core/conflict-decisions.js +21 -0
  18. package/lib/core/crash-report.d.ts +15 -12
  19. package/lib/core/crash-report.js +33 -40
  20. package/lib/core/plugin-cli.d.ts +34 -6
  21. package/lib/core/plugin-cli.js +111 -15
  22. package/lib/core/types.d.ts +9 -0
  23. package/lib/index.d.ts +4 -3
  24. package/lib/index.js +91 -192
  25. package/lib/profiles/dsh-profile-launcher.d.ts +141 -0
  26. package/lib/profiles/dsh-profile-launcher.js +396 -0
  27. package/lib/profiles/dsh-profile-manager.d.ts +49 -15
  28. package/lib/profiles/dsh-profile-manager.js +151 -48
  29. package/lib/profiles/dsh-profile-runtime.d.ts +83 -0
  30. package/lib/profiles/dsh-profile-runtime.js +194 -0
  31. package/lib/profiles/dsh-profile-shared.d.ts +117 -10
  32. package/lib/profiles/dsh-profile-shared.js +12 -0
  33. package/lib/profiles/index.d.ts +10 -3
  34. package/lib/profiles/index.js +10 -3
  35. package/lib/profiles/process-control.d.ts +37 -0
  36. package/lib/profiles/process-control.js +75 -0
  37. package/lib/routes/kit.d.ts +1 -0
  38. package/lib/routes/profiles.d.ts +1 -1
  39. package/lib/routes/profiles.js +140 -20
  40. package/lib/schema/section-registry.d.ts +24 -2
  41. package/lib/schema/section-registry.js +27 -0
  42. package/lib/ui/dsh-profiles-view.d.ts +52 -9
  43. package/lib/ui/dsh-profiles-view.js +80 -13
  44. package/lib/ui/i18n.d.ts +0 -1
  45. package/lib/ui/i18n.js +0 -2
  46. package/lib/ui/import-wizard.d.ts +30 -3
  47. package/lib/ui/import-wizard.js +30 -6
  48. package/lib/ui/overview-view.d.ts +12 -0
  49. package/lib/ui/overview-view.js +10 -0
  50. package/lib/ui/select-model.d.ts +51 -0
  51. package/lib/ui/select-model.js +84 -0
  52. package/lib/ui/selection-model.d.ts +28 -1
  53. package/lib/ui/selection-model.js +48 -3
  54. package/lib/utils/env-lock.d.ts +39 -0
  55. package/lib/utils/env-lock.js +121 -25
  56. package/package.json +17 -15
  57. package/src/adapters/plugins.ts +3 -3
  58. package/src/cli/index.ts +26 -9
  59. package/src/client/ConfigManagerSection.tsx +12 -39
  60. package/src/client/about/AboutPanel.tsx +22 -8
  61. package/src/client/about/about-view.test.ts +53 -6
  62. package/src/client/about/about-view.ts +26 -2
  63. package/src/client/api.ts +36 -6
  64. package/src/client/client-types.ts +2 -2
  65. package/src/client/common/ConfirmDialog.tsx +3 -2
  66. package/src/client/common/ContentPicker.tsx +21 -17
  67. package/src/client/common/CopyButton.tsx +74 -0
  68. package/src/client/common/ErrorBanner.tsx +3 -2
  69. package/src/client/common/Icon.tsx +116 -0
  70. package/src/client/common/Modal.tsx +29 -8
  71. package/src/client/common/Motion.tsx +70 -0
  72. package/src/client/common/ReportView.tsx +2 -2
  73. package/src/client/common/RunsCenter.tsx +3 -2
  74. package/src/client/common/Select.tsx +223 -0
  75. package/src/client/common/Skeleton.tsx +128 -0
  76. package/src/client/common/client-route-parity.test.ts +10 -10
  77. package/src/client/common/http-usage-guard.test.ts +1 -1
  78. package/src/client/common/http.test.ts +1 -1
  79. package/src/client/common/http.ts +2 -2
  80. package/src/client/common/morph-icons.test.ts +206 -0
  81. package/src/client/common/morph-icons.ts +89 -0
  82. package/src/client/common/routes.ts +10 -12
  83. package/src/client/common/ui.tsx +48 -5
  84. package/src/client/config-manager.module.css +550 -34
  85. package/src/client/history/HistoryPanel.tsx +30 -29
  86. package/src/client/icon-layer-guard.test.ts +137 -0
  87. package/src/client/import/ImportWizardView.tsx +15 -4
  88. package/src/client/import/use-import-wizard-controller.ts +24 -7
  89. package/src/client/index.ts +4 -4
  90. package/src/client/locales.ts +126 -125
  91. package/src/client/lucide-icons.d.ts +11 -0
  92. package/src/client/market/MarketPanel.tsx +39 -37
  93. package/src/client/market/MyConfigsInstall.tsx +3 -2
  94. package/src/client/market/MyConfigsList.tsx +2 -1
  95. package/src/client/market/MyConfigsLoginCard.tsx +3 -2
  96. package/src/client/overview/OverviewPanel.tsx +16 -40
  97. package/src/client/profiles/ProfilesPanel.tsx +422 -215
  98. package/src/client/recovery/RecoveryPanel.tsx +120 -3
  99. package/src/client/recovery/incident-api.ts +91 -0
  100. package/src/client/recovery/recovery-locales.ts +56 -0
  101. package/src/client/recovery/recovery-view.ts +71 -0
  102. package/src/client/run-store.test.ts +174 -2
  103. package/src/client/run-store.ts +147 -28
  104. package/src/client/snapshots/BackupScheduleCard.tsx +32 -35
  105. package/src/client/snapshots/RestorePlanView.tsx +8 -5
  106. package/src/client/snapshots/SnapshotsPanel.tsx +13 -4
  107. package/src/client/sync/AutosyncCard.tsx +6 -10
  108. package/src/client/sync/ChannelConfigDialog.tsx +8 -10
  109. package/src/client/sync/SyncHistoryView.tsx +3 -2
  110. package/src/client/sync/SyncSettingsView.tsx +17 -18
  111. package/src/core/analyzer.ts +8 -21
  112. package/src/core/backup.ts +2 -2
  113. package/src/core/boot-paths.test.ts +35 -0
  114. package/src/core/boot-paths.ts +29 -0
  115. package/src/core/boot-rescue.test.ts +82 -0
  116. package/src/core/boot-rescue.ts +57 -1
  117. package/src/core/boot-safety.ts +2 -2
  118. package/src/core/conflict-decisions.ts +34 -0
  119. package/src/core/crash-report.test.ts +25 -41
  120. package/src/core/crash-report.ts +34 -41
  121. package/src/core/incident-wiring.test.ts +96 -0
  122. package/src/core/plugin-cli.fs.test.ts +52 -0
  123. package/src/core/plugin-cli.test.ts +49 -8
  124. package/src/core/plugin-cli.ts +106 -13
  125. package/src/core/smoke.test.ts +4 -1
  126. package/src/core/types.ts +9 -0
  127. package/src/index.ts +94 -168
  128. package/src/profiles/dsh-profile-launcher.test.ts +359 -0
  129. package/src/profiles/dsh-profile-launcher.ts +473 -0
  130. package/src/profiles/dsh-profile-manager.test.ts +123 -59
  131. package/src/profiles/dsh-profile-manager.ts +148 -51
  132. package/src/profiles/dsh-profile-runtime.test.ts +196 -0
  133. package/src/profiles/dsh-profile-runtime.ts +228 -0
  134. package/src/profiles/dsh-profile-shared.ts +126 -9
  135. package/src/profiles/index.ts +30 -4
  136. package/src/profiles/process-control.ts +99 -0
  137. package/src/routes/kit.ts +1 -0
  138. package/src/routes/profiles.ts +139 -18
  139. package/src/schema/registry.test.ts +36 -0
  140. package/src/schema/section-registry.ts +42 -2
  141. package/src/ui/dsh-profiles-view.test.ts +87 -12
  142. package/src/ui/dsh-profiles-view.ts +106 -17
  143. package/src/ui/i18n.test.ts +0 -2
  144. package/src/ui/i18n.ts +0 -2
  145. package/src/ui/import-wizard.test.ts +40 -2
  146. package/src/ui/import-wizard.ts +41 -7
  147. package/src/ui/overview-view.test.ts +9 -0
  148. package/src/ui/overview-view.ts +16 -0
  149. package/src/ui/select-model.test.ts +75 -0
  150. package/src/ui/select-model.ts +92 -0
  151. package/src/ui/selection-model.test.ts +47 -5
  152. package/src/ui/selection-model.ts +50 -3
  153. package/src/utils/env-lock.test.ts +120 -0
  154. package/src/utils/env-lock.ts +140 -21
  155. package/lib/core/config-lifecycle.d.ts +0 -179
  156. package/lib/core/config-lifecycle.js +0 -479
  157. package/lib/core/config-snapshot.d.ts +0 -137
  158. package/lib/core/config-snapshot.js +0 -443
  159. package/lib/core/config-state.d.ts +0 -70
  160. package/lib/core/config-state.js +0 -177
  161. package/lib/core/undo.d.ts +0 -73
  162. package/lib/core/undo.js +0 -87
  163. package/lib/core/watcher.d.ts +0 -101
  164. package/lib/core/watcher.js +0 -219
  165. package/src/client/lifecycle/LifecyclePanel.tsx +0 -525
  166. package/src/client/lifecycle/lifecycle-api.ts +0 -178
  167. package/src/core/config-lifecycle.test.ts +0 -980
  168. package/src/core/config-lifecycle.ts +0 -571
  169. package/src/core/config-snapshot.test.ts +0 -559
  170. package/src/core/config-snapshot.ts +0 -538
  171. package/src/core/config-state.test.ts +0 -381
  172. package/src/core/config-state.ts +0 -207
  173. package/src/core/phase1-wiring.test.ts +0 -148
  174. package/src/core/undo.ts +0 -116
  175. package/src/core/watcher.test.ts +0 -326
  176. package/src/core/watcher.ts +0 -263
package/README.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # 🎒 DSH Config Manager
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/dsh-config-manager?label=npm)](https://www.npmjs.com/package/dsh-config-manager)
4
+ [![npm downloads](https://img.shields.io/npm/dm/dsh-config-manager?label=downloads%2Fmonth)](https://www.npmjs.com/package/dsh-config-manager)
5
+ [![GitHub stars](https://img.shields.io/github/stars/xiajiajun516/dsh-config-manager?label=stars)](https://github.com/xiajiajun516/dsh-config-manager/stargazers)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](https://github.com/xiajiajun516/dsh-config-manager/blob/main/LICENSE)
7
+ [![DSH plugin](https://img.shields.io/badge/DSH-plugin-blueviolet)](https://github.com/deepseek-ai/deepseek-harness)
8
+
3
9
  **DeepSeek Harness Backup, Restore & Migration Plugin.**
4
10
 
5
11
  Backup, restore, export, import, migrate and sync your complete DeepSeek Harness (DSH) configuration — settings, model providers, plugins, MCP servers, skills, agent presets and workspaces — and restore your whole environment on a new machine with one click.
@@ -64,6 +70,21 @@ Browse the built-in official market for ready-made configurations (model provide
64
70
 
65
71
  ---
66
72
 
73
+ ## 🆚 How it differs from the other DSH backup / sync plugins
74
+
75
+ Several DSH plugins live in this space and they solve different problems — pick the one that matches your situation; they can also coexist.
76
+
77
+ | Plugin | Strongest at | Where DSH Config Manager goes further |
78
+ |---|---|---|
79
+ | [xiaoyuyu6420/dsh-backup](https://github.com/xiaoyuyu6420/dsh-backup) | One-command `~/.dsh` snapshots from the CLI, plus session doctor / upgrade snapshots / rescue console | Review-before-write GUI flow (dry-run preview, per-item conflict decisions, automatic rollback), cross-machine path remapping, encrypted credential payload, configuration marketplace |
80
+ | [muyifc/dsh-config-sync](https://github.com/muyifc/dsh-config-sync) | Export / import DSH configuration to a portable, password-encrypted file, callable from tool calls | 13–14 sections incl. plugins / MCP / skills / profiles / workspaces, scheduled backups, Git + WebDAV sync per channel, session migration with path rebase |
81
+ | [dickpy/dsh-cloud-sync](https://github.com/dickpy/dsh-cloud-sync) · [weibaohui/dsh-sync](https://github.com/weibaohui/dsh-sync) | Keeping machines consistent through WebDAV / S3 or a private Git mirror | Sync is one of five capabilities here — alongside export/import, scheduling, marketplace and profile instance launch/stop |
82
+ | `cp -r ~/.dsh` (or Git on the home dir) | Free, zero setup, fine for a purely textual config | No secret handling, no path remapping, no capture of `link:` / `file:` plugin installs, no session-log work, no conflict handling or rollback |
83
+
84
+ **Short version**: for a one-command snapshot of everything, `dsh-backup` is excellent. If what you want is *move this working environment to another machine — and keep it in sync — with a review step before anything is written*, that is exactly what this plugin is for.
85
+
86
+ ---
87
+
67
88
  ## ✨ Highlights
68
89
 
69
90
  | Icon | Feature | In one line |
@@ -79,7 +100,7 @@ Browse the built-in official market for ready-made configurations (model provide
79
100
  | 🔄 | **Remote Sync** | Push/pull portable config via **Git private repo or WebDAV** (secrets do not sync by default; encrypted snapshots can optionally carry encrypted credentials) |
80
101
  | ⏰ | **Scheduled backups** | Full backup on a fixed cadence (6h / 12h / 24h / 7d) — set-and-forget, secrets never included |
81
102
  | 🛒 | **Config Marketplace** | Browse & one-click install community configs — supply-chain warnings + per-item content selection (change summary + in-place high-risk flags) |
82
- | 🗂️ | **Profiles (DSH profiles)** | Manage `$DSH_HOME/profiles/<name>` directly: list / create from a shipped template / rename / hard delete / record which profile the next launch should use |
103
+ | 🗂️ | **Profiles (DSH profiles)** | Manage `$DSH_HOME/profiles/<name>` directly: list / create from a shipped template / rename / hard delete / **launch this profile (independent instance)** / **stop the instance** (the row button flips between Launch and Stop with the running state) |
83
104
  | 🌐 | **Bilingual UI** | Interface, reports and error details follow the DSH app language (中文 / English) |
84
105
  | 🤖 | **Agent tools** | Backup / snapshot / restore / sync right from an agent session |
85
106
 
@@ -140,12 +161,32 @@ dsh plugin --profile web add dsh-config-manager@latest
140
161
 
141
162
  > 💡 Just copy-paste the command: `@latest` ensures you get the newest build.
142
163
  >
143
- > 🐛 **`@latest` installed an old version?** That's pnpm 11's `minimumReleaseAge` supply-chain policy, not a cache issue: versions published less than ~30 days ago are excluded from resolution until whitelisted. Two fixes:
144
- > - Install an exact version once (it auto-whitelists, then `@latest` works):
145
- > ```bash
146
- > dsh plugin --profile web add dsh-config-manager@0.1.8
147
- > ```
148
- > - Or disable the age gate with a one-liner (adds `minimumReleaseAge: 0` at the top of the profile's `pnpm-workspace.yaml`):
164
+ > 🐛 **If `@latest` installed an old version**: that's pnpm's `minimumReleaseAge` supply-chain policy (not a cache issue). The gate is evaluated **per version, at resolution time**: a release younger than the threshold (~30 days) stays invisible to `@latest` until it ages past it — and the clock restarts for every future release. Installing an exact version once fixes *that one version only*; the next release published is invisible to `@latest` again. It is **not** a one-time fix.
165
+ >
166
+ > **Permanent fix (recommended)** — exempt this single package from the age gate. Add to the profile's `pnpm-workspace.yaml` (`~/.dsh/profiles/web/pnpm-workspace.yaml`):
167
+ >
168
+ > ```yaml
169
+ > minimumReleaseAgeExclude:
170
+ > - dsh-config-manager
171
+ > ```
172
+ >
173
+ > After that `@latest` resolves the newest release normally — including every future release.
174
+ >
175
+ > **One-off fix** — ask npm which version is actually latest and install exactly that. Repeat it each time a newer version has been published:
176
+ >
177
+ > ```powershell
178
+ > # Windows (PowerShell)
179
+ > $v = (npm view dsh-config-manager version).Trim(); dsh plugin --profile web add "dsh-config-manager@$v"
180
+ > ```
181
+ >
182
+ > ```bash
183
+ > # macOS / Linux
184
+ > dsh plugin --profile web add "dsh-config-manager@$(npm view dsh-config-manager version)"
185
+ > ```
186
+ >
187
+ > After restarting DSH, **Settings → Backup & Migration → About** shows the version you are actually running (and pops up the release notes whenever it changes).
188
+ >
189
+ > - Or disable the age gate entirely with a one-liner (adds `minimumReleaseAge: 0` at the top of the profile's `pnpm-workspace.yaml`):
149
190
  > ```powershell
150
191
  > $f = "$env:USERPROFILE\.dsh\profiles\web\pnpm-workspace.yaml"
151
192
  > $c = Get-Content $f -Raw
@@ -276,11 +317,18 @@ launched with `dsh --profile <name>`. This page reads and writes that directory
276
317
  | Create | Writes the standard three files under `$DSH_HOME/profiles/<name>` (equivalent to the shipped `initProfile`); starting templates: base / web / headless / sdk / sdk-minimal / acp |
277
318
  | Rename | Directory move + fixes the manifest name field; the running profile is refused |
278
319
  | Delete | **Hard-deletes the whole directory** (including node_modules); deleting the running profile needs an extra checkbox |
279
- | Next launch | Only records which profile to use next and shows the `dsh --profile <name>` restart command |
280
-
281
- > DSH **cannot switch profiles while running** (the profile comes from the launch flag and bundle layers resolve at boot),
282
- > so this page never touches processes — it records your choice and asks you to restart.
283
- > Third-party plugins must be installed into that profile separately (`dsh plugin --profile <name> add <pkg>`).
320
+ | Launch this profile | The **switch that actually works**: starts an **independent DSH instance** for that profile (free port picked automatically, browser opened automatically); the running instance and its tasks are untouched. Web-shaped profiles only — non-web ones (e.g. the base template) have no browser UI, so the page shows the terminal command instead of failing silently |
321
+ | Stop instance | While that profile has a running instance, the row button flips from Launch to Stop (the Runtime card also offers one); it asks the process to exit first (grace period) and terminates the process tree only after that, reporting which path was used. **Deleting a profile with a running instance is refused** |
322
+ | No duplicate launches | “Which profiles are running” = the ledger of instances this plugin started ∪ **each instance's own heartbeat** (`<dataDir>/running/<profile>.json`, pid/port only — **never the auth token**). So even a profile you started by hand with `dsh web` will not be launched a second time (you get a clear “already running” message); the instance you are using right now offers no Stop button (that would kill your own session — close that window/terminal, or stop it from the instance that launched it) |
323
+
324
+ > **Why there is no "set as next launch"** (button and marker removed in 2026-09): DSH has **no "default / next launch
325
+ > profile" state** — the profile comes only from the launch arguments (`dsh <name>` / `--profile <name>`; `dsh web` is a
326
+ > hard-coded alias), so any "which profile to use next" marker has **no consumer at all**: the `dsh web` you type
327
+ > yourself still boots web after a restart. Only two mechanisms really switch profiles: ① this page's "launch this
328
+ > profile" (an extra instance, no interruption, stoppable at any time), ② pointing your launch command/shortcut at
329
+ > `dsh --profile <name>` (ecosystem tools such as dshm and DSH Launcher all spawn instances from an external launcher).
330
+ > Instances started by this plugin are recorded in `<dataDir>/launches.json` (pid/port/log), which is why they can be
331
+ > stopped; third-party plugins must be installed into that profile separately (`dsh plugin --profile <name> add <pkg>`).
284
332
  > The tab lives at Settings → "Backup & Migration" → **Profiles**.
285
333
 
286
334
  ### 📸 Snapshot restore (undo an import)
@@ -540,6 +588,24 @@ Yes. The import wizard asks for the export-time encryption password and verifies
540
588
  5. **Encrypted backups**: a lost password means the `secrets.enc` can't be decrypted (by design — keep your password safe)
541
589
  6. **Snapshot restore is offline and honest**: entries the offline engine can't restore (settings namespaces / patch lines when the snapshot has no whole-file backup, workspace records stored in DSH storages) are reported as skipped with a pointer to online rollback; credential **values** are never auto-written (manual re-entry hint only); old snapshots without a plugin baseline only get a hint to remove added plugins manually
542
590
 
591
+ ## 💬 Feedback
592
+
593
+ Found a bug, a misaligned panel, a button that does nothing — or just have an idea? **All of it is welcome.** UI problems especially: they are the easiest thing to overlook and the part that real usage should decide.
594
+
595
+ | What you want to say | Where |
596
+ |---|---|
597
+ | 🎨 **UI problem**: misaligned layout, broken styling, dark mode, scaling, a control that does nothing | [UI issue form](https://github.com/xiajiajun516/dsh-config-manager/issues/new?template=ui_bug.en.yml) — screenshot + browser version is enough |
598
+ | 🐛 **Something is broken / an error / wrong data** | [Bug report](https://github.com/xiajiajun516/dsh-config-manager/issues/new?template=bug_report.en.yml) |
599
+ | ✨ **New feature idea** | [Feature request](https://github.com/xiajiajun516/dsh-config-manager/issues/new?template=feature_request.en.yml) |
600
+ | 💬 **Not sure whether it is a bug — just asking** | [Discussions](https://github.com/xiajiajun516/dsh-config-manager/discussions) |
601
+ | 🔒 **Security issue / leaked credential** | [Private security advisory](https://github.com/xiajiajun516/dsh-config-manager/security/advisories/new) (please do not open a public issue) |
602
+
603
+ **Report straight from the plugin**: Settings → Backup & Migration → **About** → "Issues"; or hit **Copy environment info** on that page — plugin version / DSH version / platform are included, so you can paste it into the issue instead of typing version numbers.
604
+
605
+ **Every report gets followed up**: a new issue receives an immediate reply and the `needs-triage` label, and progress is visible in the labels (`needs-info` → `confirmed` → `fixed`). Fixed problems end up in [CHANGELOG.md](CHANGELOG.md) under the release that fixed them, tagged with the issue number (e.g. #38 / #43 / #45) — that is where a report finally lands.
606
+
607
+ > ⚠️ Please search for an existing issue first, and **strip every API key / token / password** — including the ones visible in screenshots and logs.
608
+
543
609
  ## 🙏 Contributors
544
610
 
545
611
  - **lux-liang (Jialiang Liang)** — [PR #44](https://github.com/xiajiajun516/dsh-config-manager/pull/44): independently fixed the
package/README.zh-CN.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # 🎒 DSH Config Manager
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/dsh-config-manager?label=npm)](https://www.npmjs.com/package/dsh-config-manager)
4
+ [![npm downloads](https://img.shields.io/npm/dm/dsh-config-manager?label=downloads%2Fmonth)](https://www.npmjs.com/package/dsh-config-manager)
5
+ [![GitHub stars](https://img.shields.io/github/stars/xiajiajun516/dsh-config-manager?label=stars)](https://github.com/xiajiajun516/dsh-config-manager/stargazers)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](https://github.com/xiajiajun516/dsh-config-manager/blob/main/LICENSE)
7
+ [![DSH plugin](https://img.shields.io/badge/DSH-plugin-blueviolet)](https://github.com/deepseek-ai/deepseek-harness)
8
+
3
9
  **DeepSeek Harness(DSH)配置备份、恢复与迁移插件。**
4
10
 
5
11
  DSH Config Manager 是一个 DeepSeek Harness 配置备份与迁移插件:一键备份、恢复、导出、导入和迁移完整 DSH 配置,包括——
@@ -13,7 +19,7 @@ DSH Config Manager 是一个 DeepSeek Harness 配置备份与迁移插件:一
13
19
  - Workspace / AGENTS.md
14
20
 
15
21
  > DSH 的 profile(`$DSH_HOME/profiles/<name>`)本身**不随备份迁移**(它是「用哪套插件组合启动」的机器本地选择);
16
- > 本插件的「档案」页可以列表 / 新建 / 重命名 / 删除它们,并记录「下次启动用哪个」。
22
+ > 本插件的「档案」页可以列表 / 新建 / 重命名 / 删除它们,也可以**用某个档案另起一个独立实例并随时停止它**(真正可用的切换)。
17
23
 
18
24
  把当前 DeepSeek Harness 环境导出为可移植备份,在另一台电脑上一键恢复;也支持通过 Git / WebDAV 跨机同步(密钥默认不同步,勾选「导出密钥」并加密后可随加密快照迁移);还能通过内置**配置市场**浏览、一键安装社区分享的现成配置。
19
25
 
@@ -66,6 +72,21 @@ DSH 是你的 AI 助手工作台,里面存着你的各种设置:模型配置
66
72
 
67
73
  ---
68
74
 
75
+ ## 🆚 与其它备份 / 同步插件的区别
76
+
77
+ DSH 生态里这个方向有几个插件,它们解决的问题并不相同——按自己的场景选一个即可,也可以共存。
78
+
79
+ | 插件 | 最擅长 | 本插件更进一步的地方 |
80
+ |---|---|---|
81
+ | [xiaoyuyu6420/dsh-backup](https://github.com/xiaoyuyu6420/dsh-backup) | 一条命令从 CLI 给整个 `~/.dsh` 打快照,另有会话体检 / 升级快照 / 救援控制台 | 写盘前可审阅的 GUI 流程(dry-run 预览、冲突逐项决策、失败自动回滚)、跨机路径重映射、加密凭据载荷、配置市场 |
82
+ | [muyifc/dsh-config-sync](https://github.com/muyifc/dsh-config-sync) | 把 DSH 配置导出/导入成可移植的密码加密文件,可被工具调用 | 13–14 个分区(含插件 / MCP / 技能 / 档案 / 工作区)、定时备份、Git + WebDAV 双通道同步、会话跨机迁移与路径重定基 |
83
+ | [dickpy/dsh-cloud-sync](https://github.com/dickpy/dsh-cloud-sync) · [weibaohui/dsh-sync](https://github.com/weibaohui/dsh-sync) | 通过 WebDAV / S3 或私有 Git 镜像让多台机器保持一致 | 同步只是本插件五项能力之一——另有导出/导入、定时备份、配置市场与档案实例启停 |
84
+ | `cp -r ~/.dsh`(或给 home 目录挂 Git) | 免费、零配置,纯文本配置够用 | 不处理密钥、不做路径重映射、抓不到 `link:` / `file:` 安装的本地插件、不动会话日志、没有冲突处理与回滚 |
85
+
86
+ **一句话**:想要「一条命令把一切打快照」,`dsh-backup` 很好用;想要「把一整套能用的环境搬到另一台电脑、并持续同步,而且写盘前一定先给你看」,那就是本插件。
87
+
88
+ ---
89
+
69
90
  ## ✨ 核心亮点
70
91
 
71
92
  | 图标 | 功能 | 一句话说明 |
@@ -81,7 +102,7 @@ DSH 是你的 AI 助手工作台,里面存着你的各种设置:模型配置
81
102
  | 🔄 | **远程同步** | 通过 **Git 私有仓库或 WebDAV** 推送 / 拉取可移植配置(密钥默认不参与同步;加密快照可选携带密文凭据) |
82
103
  | ⏰ | **定时全量备份** | 按固定周期(6h / 12h / 24h / 7d)自动全量备份,一劳永逸,密钥永不包含 |
83
104
  | 🛒 | **配置市场** | 浏览并一键安装社区分享的配置——供应链警示 + 逐项内容选择(可就地看到改动与高风险分区) |
84
- | 🗂️ | **档案 Profiles(DSH profile)** | 直接管理 `$DSH_HOME/profiles/<name>`:列表 / 新建(官方模板)/ 重命名 / 物理删除 / 记录「下次启动用哪个」 |
105
+ | 🗂️ | **档案 Profiles(DSH profile)** | 直接管理 `$DSH_HOME/profiles/<name>`:列表 / 新建(官方模板)/ 重命名 / 物理删除 / **启动该档案(独立实例)** / **停止实例**(行内按钮按运行状态在「启动 ↔ 停止」间切换) |
85
106
  | 🧩 | **本地插件随备份迁移** | `link:` / `file:` 安装的本地开发插件会被打包进备份,换机不再丢失 |
86
107
  | 🗄️ | **保留策略可配(GFS 分层)** | 「最近 N 份 + 每月留 1 份 + 每年留 1 份」,默认值等价旧行为 |
87
108
  | 🤖 | **Agent 工具** | Agent 会话内直接备份 / 快照 / 恢复 / 同步 |
@@ -143,11 +164,31 @@ dsh plugin --profile web add dsh-config-manager@latest
143
164
 
144
165
  > 💡 照着复制就行:`@latest` 确保装到最新版。
145
166
  >
146
- > 🐛 **`@latest` 装到了旧版?** 这是 **pnpm 11 的 `minimumReleaseAge` 供应链发布年龄策略**(不是缓存):发布不足约 30 天的新版本会被排除出版本解析,直到进入白名单。两种解决办法:
147
- > - 装一次精确版本即可自动白名单,之后 `@latest` 正常:
148
- > ```bash
149
- > dsh plugin --profile web add dsh-config-manager@0.1.8
150
- > ```
167
+ > 🐛 **`@latest` 装到了旧版?** 这是 **pnpm 的 `minimumReleaseAge` 供应链发布年龄策略**(不是缓存)。这个门槛**按版本、在解析时**判定:发布不足阈值(约 30 天)的版本对 `@latest` 不可见,直到它「变老」——而**每个新版本都会重新计一次**。所以「装一次精确版本就永久正常」是错的:它只解决当时那一个版本,之后作者一发新版,`@latest` 又会装到旧版,**不是一次性修复**。
168
+ >
169
+ > **永久解决(推荐)**——只把这一个包排除在年龄门槛之外。在 profile 的 `pnpm-workspace.yaml`(`~/.dsh/profiles/web/pnpm-workspace.yaml`)里加上:
170
+ >
171
+ > ```yaml
172
+ > minimumReleaseAgeExclude:
173
+ > - dsh-config-manager
174
+ > ```
175
+ >
176
+ > 之后 `@latest` 会一直解析到最新发布版,包括以后再发的新版本。
177
+ >
178
+ > **临时解决**——先问 npm「当前真正的最新版本是多少」,再按精确版本安装;每当有新版本发布都要重跑一次:
179
+ >
180
+ > ```powershell
181
+ > # Windows(PowerShell)
182
+ > $v = (npm view dsh-config-manager version).Trim(); dsh plugin --profile web add "dsh-config-manager@$v"
183
+ > ```
184
+ >
185
+ > ```bash
186
+ > # macOS / Linux
187
+ > dsh plugin --profile web add "dsh-config-manager@$(npm view dsh-config-manager version)"
188
+ > ```
189
+ >
190
+ > 重启 DSH 后,可在 **设置 → 备份与迁移 → 关于** 看到实际运行的版本(版本变化时也会自动弹出更新内容)。
191
+ >
151
192
  > - 或一行命令彻底关闭年龄门槛(在 profile 的 `pnpm-workspace.yaml` 顶部加 `minimumReleaseAge: 0`):
152
193
  > ```powershell
153
194
  > $f = "$env:USERPROFILE\.dsh\profiles\web\pnpm-workspace.yaml"
@@ -272,10 +313,17 @@ dsh plugin --profile web add dsh-config-manager@latest
272
313
  | 新建 | 在 `$DSH_HOME/profiles/<name>` 写标准三件套(与官方 `initProfile` 等价);可选起步模板 base / web / headless / sdk / sdk-minimal / acp |
273
314
  | 重命名 | 目录级移动 + 同步修正 `package.json` 的 name;当前运行中的档案拒绝重命名 |
274
315
  | 删除 | **物理删除整个目录**(含 node_modules);当前运行中的档案需额外勾选确认 |
275
- | 下次启动 | 只写「下次启动用哪个」标记 + 给出 `dsh --profile <name>` 重启命令 |
276
-
277
- > DSH **无法在运行中切换 profile**(profile 由启动参数决定,bundle 层在启动时解析),因此本页不做进程操作,
278
- > 只记录选择并提示你手动重启。第三方插件需要在该档案里单独安装(`dsh plugin --profile <name> add <pkg>`)。
316
+ | 启动该档案 | **真正可用的切换**:以该档案另起一个**独立 DSH 实例**(自动挑空闲端口 + 自动打开浏览器),当前实例与正在跑的任务不受影响;只支持 **web 形态**档案:非 web 形态(如 base 模板)没有浏览器界面,点击后给出终端命令而不是静默失败 |
317
+ | 停止实例 | 该档案有实例在跑时,行内「启动」自动变成「停止」(运行状态卡里也有一个);先请它自己退出(优雅期),超时才结束进程树,并如实告诉你是哪种;**实例运行中拒绝物理删除该档案** |
318
+ | 不会重复启动 | 「哪些档案在跑」= 本插件启动的实例台账 ∪ **每个实例自报的心跳**(`<dataDir>/running/<profile>.json`,只含 pid/端口,**不含认证 token**)→ 哪怕某个档案是你手动 `dsh web` 起来的,本页也不会再启动第二个同名实例(明确提示已在运行);**当前这个实例本身**不给停止按钮(停自己会把你自己杀掉——请关窗口/终端,或到启动它的那个实例里停止) |
319
+
320
+ > **为什么没有「设为下次启动」**(2026-09 该按钮与标记一并移除):DSH **没有「默认 / 下次启动 profile」这种状态** ——
321
+ > profile 只由启动参数决定(`dsh <名>` / `--profile <名>`,`dsh web` 是硬编码别名),任何「下次启动用哪个」的标记
322
+ > **都没有消费者**:你自己敲的 `dsh web` 重启后当然还是 web。真正可用的切换只有两种:① 本页的「启动该档案」
323
+ > 另起一个实例(不打断当前会话,随时可停);② 把启动命令/快捷方式换成 `dsh --profile <名>`
324
+ > (生态里的 dshm、DSH Launcher 也都是「外部启动器 spawn 实例」这一条路)。
325
+ > 本插件启动的实例记在 `<dataDir>/launches.json`(pid/端口/日志),所以**关得掉**;
326
+ > 第三方插件需要在该档案里单独安装(`dsh plugin --profile <name> add <pkg>`)。
279
327
 
280
328
  ### 📸 快照恢复(撤销一次导入)
281
329
 
@@ -458,6 +506,24 @@ dsh-config-manager backup --sections skills,self # 收窄范围
458
506
  6. **快照恢复是离线的、诚实的**:离线引擎无法恢复的条目(快照无整文件备份时的 settings namespace / patch 行、存在 DSH storages 里的 workspace 记录)会如实列为跳过并指向在线回滚;凭据**值**绝不自动改写(只提示人工补录);无插件基线的旧快照只提示人工核对新增插件
459
507
  7. **本地源插件(`link:` / `file:`)随备份打包**:导出时执行 `npm pack` 把本地开发中的插件打成 tarball 一并备份,导入时解包到 `$DSH_HOME/dsh-config-manager/local-plugins/` 后按 `file:` 安装。因此:① 备份体积会随本地插件的体积增大(单插件超过 100 MB 会被跳过并告警,建议先发布到 registry / git 再备份);② 插件**源码**会进入备份(与「密钥永不进备份」不冲突——密钥仍被排除,这里进的是代码);③ 打包需要本机有可用的 `npm`,无 npm 时该插件退化为原行为(保留原 spec,换机后仍需手工安装)
460
508
 
509
+ ## 💬 反馈与建议
510
+
511
+ 遇到 Bug、界面错位、按钮点了没反应,或者只是有个想法 —— **都欢迎提出来**。界面问题尤其欢迎:这是最容易被自己忽略、也最该由真实使用场景决定的部分。
512
+
513
+ | 你想说什么 | 去哪儿 |
514
+ |---|---|
515
+ | 🎨 **界面问题**:布局错位、样式异常、深色模式、缩放、按钮无响应 | [UI 问题表单](https://github.com/xiajiajun516/dsh-config-manager/issues/new?template=ui_bug.yml)(只要截图 + 浏览器版本) |
516
+ | 🐛 **功能出错 / 报错 / 数据不对** | [Bug 报告](https://github.com/xiajiajun516/dsh-config-manager/issues/new?template=bug_report.yml) |
517
+ | ✨ **新功能建议** | [功能建议](https://github.com/xiajiajun516/dsh-config-manager/issues/new?template=feature_request.yml) |
518
+ | 💬 **不确定是不是 Bug,想先问问** | [Discussions](https://github.com/xiajiajun516/dsh-config-manager/discussions) |
519
+ | 🔒 **安全问题 / 密钥泄露** | [私密安全公告](https://github.com/xiajiajun516/dsh-config-manager/security/advisories/new)(请不要开公开 issue) |
520
+
521
+ **在插件里就地反馈**:设置 → 备份与迁移 → **关于** → 「反馈问题」;或点「关于」里的**复制环境信息**按钮,插件版本 / DSH 版本 / 平台会自动带上,粘进 issue 即可 —— 不用手抄版本号。
522
+
523
+ **每条都会跟进**:新 issue 会立刻收到一条回复并打上 `needs-triage`,处理进度体现在标签上(`needs-info` → `confirmed` → `fixed`)。修好的问题会出现在 [CHANGELOG.md](CHANGELOG.md) 的对应版本条目里,带 issue 编号(例:#38 / #43 / #45)—— 这就是一条反馈最终的落地记录。
524
+
525
+ > ⚠️ 提交前请先搜一下是否已有同类 issue,并**抹掉任何 API Key / Token / 密码**(截图和日志里也算)。
526
+
461
527
  ## 🙏 贡献者
462
528
 
463
529
  - **lux-liang (Jialiang Liang)** —— [PR #44](https://github.com/xiajiajun516/dsh-config-manager/pull/44):独立修复了 issue #43
@@ -12,7 +12,7 @@
12
12
  */
13
13
  import { isDeepStrictEqual } from 'node:util';
14
14
  import { sha256Hex } from '../utils/hashing.js';
15
- import { installSpecFor, resolveProfileNameFromArgv } from '../core/plugin-cli.js';
15
+ import { installSpecFor, resolveProcessProfileName } from '../core/plugin-cli.js';
16
16
  import { LOCAL_PLUGIN_DIR } from '../core/local-plugin-pack.js';
17
17
  import { msgOf, zhMsg } from '../core/messages.js';
18
18
  import { isPathSafe, normalizePath } from '../utils/paths.js';
@@ -592,7 +592,7 @@ export class PluginsAdapter {
592
592
  }
593
593
  // 执行日志:记录实际将发起的子进程命令行(与宿主 DshPluginsFacade 的
594
594
  // dsh plugin --profile <p> add <spec> 一致);仅非敏感文本,渲染前 UI 再 redact 兜底
595
- ctx.onLog?.(`$ dsh plugin --profile ${ctx.target.profile ?? resolveProfileNameFromArgv()} add ${installSpecFor(name, spec)}`);
595
+ ctx.onLog?.(`$ dsh plugin --profile ${ctx.target.profile ?? resolveProcessProfileName()} add ${installSpecFor(name, spec)}`);
596
596
  // 透传中止信号:用户「跳过当前插件」→ 宿主 kill 子进程 + 清半装状态 → 抛 ImportUserSkippedError
597
597
  const result = await ctx.target.plugins.install(name, spec, ctx.signal);
598
598
  const suffix = result.needsRestart ? msg('adapter.pluginRestartSuffix') : '';
@@ -614,7 +614,7 @@ export class PluginsAdapter {
614
614
  warning: true,
615
615
  // 保留 warning(§34.17 非致命):一个装不上的插件不得拖垮已成功导入的其余配置;
616
616
  // message 附可复制的手动安装命令(profile 解析与 M1 宿主一致)。
617
- message: (item.kind === 'Update' ? msg('adapter.pluginUpdateFailed', { name, msg: reason, profile: resolveProfileNameFromArgv() }) : msg('adapter.pluginInstallFailed', { name, msg: reason, profile: resolveProfileNameFromArgv() })),
617
+ message: (item.kind === 'Update' ? msg('adapter.pluginUpdateFailed', { name, msg: reason, profile: resolveProcessProfileName() }) : msg('adapter.pluginInstallFailed', { name, msg: reason, profile: resolveProcessProfileName() })),
618
618
  };
619
619
  }
620
620
  }
@@ -75,6 +75,17 @@ export declare function resolveControlRoots(opts?: {
75
75
  dataRoot?: string;
76
76
  dataDir?: string;
77
77
  }, env?: Record<string, string | undefined>): string[];
78
+ /**
79
+ * `recover-stale-lock` 的锁目录解析:在「候选控制面根」(与 SAFE MODE 同源)里挑第一个**真的躺着**
80
+ * ownership 文件的 `<root>/locks`;全都没命中 → 缺省根 `$DSH_HOME/dsh-config-manager/locks`。
81
+ *
82
+ * 为什么不能写死缺省根(issue #36 的兜底路径):宿主 `dataDir` 可被配置成非缺省值,写死会让残留锁
83
+ * 「明明在磁盘上,CLI 却报没有」→ 用户只能手工删锁文件(正是本命令要消灭的处境)。
84
+ */
85
+ export declare function resolveRecoverLocksDir(opts?: {
86
+ dataRoot?: string;
87
+ dataDir?: string;
88
+ }, env?: Record<string, string | undefined>): string;
78
89
  /**
79
90
  * Phase 3 SAFE MODE 门:任一候选控制面根下存在 durable 标记 → 返回错误文案(调用方必须拒绝执行)。
80
91
  * 返回 null = 明确未阻断。
package/lib/cli/index.js CHANGED
@@ -31,7 +31,7 @@ import { createInterface } from 'node:readline/promises';
31
31
  import { stdin as processStdin, stdout as processStdout } from 'node:process';
32
32
  import { listSnapshots, planRestore, restore, } from '../core/restore.js';
33
33
  import { REINSTALL_ITEMS, buildReinstallPlan, isWindows, detectInstalledDshVersion, writeReinstallRecoveryPoint, } from '../core/reinstall.js';
34
- import { EnvironmentLockManager, runWithMutationLock, EnvironmentLockUnavailableError } from '../utils/env-lock.js';
34
+ import { EnvironmentLockManager, runWithMutationLock, EnvironmentLockUnavailableError, OWNERSHIP_FILE } from '../utils/env-lock.js';
35
35
  import { runSessionsRepair } from './sessions-repair.js';
36
36
  import { Phase3Recovery, readSafeModeMarkerSync, safeModeMarkerPath } from '../core/phase3-host.js';
37
37
  import { verifyBackupZip, } from '../core/backup-verify.js';
@@ -157,12 +157,10 @@ export function parseCli(argv) {
157
157
  if (command === 'sessions')
158
158
  return parseCliSessions(argv);
159
159
  if (command === 'recover-stale-lock') {
160
- // recover-stale-lock:独立显式 recovery,不接受 destructive 执行参数(只能 --data-dir 定位锁目录)
161
- for (const flag of argv.slice(1)) {
162
- if (flag !== '--data-dir' && !flag.startsWith('--data-dir=') && !flag.startsWith('-')) {
163
- return { ok: false, error: `recover-stale-lock 只接受 --data-dir / accepts only --data-dir` };
164
- }
165
- }
160
+ // recover-stale-lock:独立显式 recovery,不接受 destructive 执行参数(只能 --data-dir 定位锁目录)。
161
+ // 只需 parseCliDataDir —— 它自己就会拒绝任何未知 flag。此前这里还有一道「逐 token 只许 --data-dir」的
162
+ // 前置校验,会把 `--data-dir <dir>` 的**值**也当成非法 token 拒掉(只有 `--data-dir=<dir>` 能过),
163
+ // 于是 help 里写的空格写法在这个「只剩 CLI 可用」的紧急路径上失效。
166
164
  return parseCliDataDir(argv);
167
165
  }
168
166
  const options = { command, dryRun: false, profile: 'web', yes: false, list: false, wipeConfig: false, json: false, positionals: [] };
@@ -308,6 +306,22 @@ export function resolveControlRoots(opts = {}, env = process.env) {
308
306
  add(path.join(resolveDshHome(env), 'dsh-config-manager'));
309
307
  return out;
310
308
  }
309
+ /**
310
+ * `recover-stale-lock` 的锁目录解析:在「候选控制面根」(与 SAFE MODE 同源)里挑第一个**真的躺着**
311
+ * ownership 文件的 `<root>/locks`;全都没命中 → 缺省根 `$DSH_HOME/dsh-config-manager/locks`。
312
+ *
313
+ * 为什么不能写死缺省根(issue #36 的兜底路径):宿主 `dataDir` 可被配置成非缺省值,写死会让残留锁
314
+ * 「明明在磁盘上,CLI 却报没有」→ 用户只能手工删锁文件(正是本命令要消灭的处境)。
315
+ */
316
+ export function resolveRecoverLocksDir(opts = {}, env = process.env) {
317
+ const fallback = path.join(resolveDshHome(env), 'dsh-config-manager', 'locks');
318
+ for (const root of resolveControlRoots(opts, env)) {
319
+ const candidate = path.join(root, 'locks');
320
+ if (fssync.existsSync(path.join(candidate, OWNERSHIP_FILE)))
321
+ return candidate;
322
+ }
323
+ return fallback;
324
+ }
311
325
  /**
312
326
  * Phase 3 SAFE MODE 门:任一候选控制面根下存在 durable 标记 → 返回错误文案(调用方必须拒绝执行)。
313
327
  * 返回 null = 明确未阻断。
@@ -728,11 +742,11 @@ export async function runCli(argv, io = defaultIo, env = process.env, deps = {})
728
742
  }, io);
729
743
  }
730
744
  const lockDataDir = resolveDataDir(options.dataDir, env);
731
- const lockHome = resolveDshHome(env);
732
745
  // recover-stale-lock:独立显式 recovery(只 inspect + prove stale + 原子回收;不自动、无 --force)。
733
746
  if (options.command === 'recover-stale-lock') {
747
+ // 锁目录按候选根定位(宿主 dataDir 可被配置成非缺省值,写死缺省根会「找不到锁」→ 用户只能手工删文件)。
734
748
  const lock = new EnvironmentLockManager({
735
- locksDir: path.join(lockHome, 'dsh-config-manager', 'locks'),
749
+ locksDir: resolveRecoverLocksDir({ dataRoot: options.dataRoot, dataDir: options.dataDir }, env),
736
750
  op: 'recover-stale-lock',
737
751
  target: lockDataDir,
738
752
  lockVersion: '0.1.0',