dsh-config-manager 0.1.64 → 0.1.65

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 (168) hide show
  1. package/README.md +31 -6
  2. package/README.zh-CN.md +31 -6
  3. package/lib/cli/index.d.ts +11 -0
  4. package/lib/cli/index.js +23 -9
  5. package/lib/client.d.ts +204 -154
  6. package/lib/client.js +5139 -2886
  7. package/lib/core/analyzer.d.ts +4 -0
  8. package/lib/core/analyzer.js +8 -22
  9. package/lib/core/backup.d.ts +2 -2
  10. package/lib/core/boot-paths.d.ts +14 -0
  11. package/lib/core/boot-paths.js +28 -0
  12. package/lib/core/boot-rescue.d.ts +21 -0
  13. package/lib/core/boot-rescue.js +57 -1
  14. package/lib/core/boot-safety.js +2 -2
  15. package/lib/core/conflict-decisions.d.ts +16 -0
  16. package/lib/core/conflict-decisions.js +21 -0
  17. package/lib/core/crash-report.d.ts +15 -12
  18. package/lib/core/crash-report.js +33 -40
  19. package/lib/core/plugin-cli.js +21 -1
  20. package/lib/core/types.d.ts +9 -0
  21. package/lib/index.d.ts +4 -3
  22. package/lib/index.js +75 -187
  23. package/lib/profiles/dsh-profile-launcher.d.ts +141 -0
  24. package/lib/profiles/dsh-profile-launcher.js +396 -0
  25. package/lib/profiles/dsh-profile-manager.d.ts +49 -15
  26. package/lib/profiles/dsh-profile-manager.js +151 -48
  27. package/lib/profiles/dsh-profile-runtime.d.ts +83 -0
  28. package/lib/profiles/dsh-profile-runtime.js +194 -0
  29. package/lib/profiles/dsh-profile-shared.d.ts +117 -10
  30. package/lib/profiles/dsh-profile-shared.js +12 -0
  31. package/lib/profiles/index.d.ts +10 -3
  32. package/lib/profiles/index.js +10 -3
  33. package/lib/profiles/process-control.d.ts +37 -0
  34. package/lib/profiles/process-control.js +75 -0
  35. package/lib/routes/kit.d.ts +1 -0
  36. package/lib/routes/profiles.d.ts +1 -1
  37. package/lib/routes/profiles.js +140 -20
  38. package/lib/schema/section-registry.d.ts +24 -2
  39. package/lib/schema/section-registry.js +27 -0
  40. package/lib/ui/dsh-profiles-view.d.ts +52 -9
  41. package/lib/ui/dsh-profiles-view.js +80 -13
  42. package/lib/ui/i18n.d.ts +0 -1
  43. package/lib/ui/i18n.js +0 -2
  44. package/lib/ui/import-wizard.d.ts +30 -3
  45. package/lib/ui/import-wizard.js +30 -6
  46. package/lib/ui/select-model.d.ts +51 -0
  47. package/lib/ui/select-model.js +84 -0
  48. package/lib/ui/selection-model.d.ts +28 -1
  49. package/lib/ui/selection-model.js +48 -3
  50. package/lib/utils/env-lock.d.ts +39 -0
  51. package/lib/utils/env-lock.js +121 -25
  52. package/package.json +3 -1
  53. package/src/cli/index.ts +26 -9
  54. package/src/client/ConfigManagerSection.tsx +12 -39
  55. package/src/client/about/AboutPanel.tsx +22 -8
  56. package/src/client/about/about-view.test.ts +53 -6
  57. package/src/client/about/about-view.ts +26 -2
  58. package/src/client/api.ts +36 -6
  59. package/src/client/client-types.ts +2 -2
  60. package/src/client/common/ConfirmDialog.tsx +3 -2
  61. package/src/client/common/ContentPicker.tsx +21 -17
  62. package/src/client/common/CopyButton.tsx +74 -0
  63. package/src/client/common/ErrorBanner.tsx +3 -2
  64. package/src/client/common/Icon.tsx +116 -0
  65. package/src/client/common/Modal.tsx +29 -8
  66. package/src/client/common/Motion.tsx +70 -0
  67. package/src/client/common/ReportView.tsx +2 -2
  68. package/src/client/common/RunsCenter.tsx +3 -2
  69. package/src/client/common/Select.tsx +223 -0
  70. package/src/client/common/Skeleton.tsx +128 -0
  71. package/src/client/common/client-route-parity.test.ts +10 -10
  72. package/src/client/common/http-usage-guard.test.ts +1 -1
  73. package/src/client/common/http.test.ts +1 -1
  74. package/src/client/common/http.ts +2 -2
  75. package/src/client/common/morph-icons.test.ts +206 -0
  76. package/src/client/common/morph-icons.ts +89 -0
  77. package/src/client/common/routes.ts +10 -12
  78. package/src/client/common/ui.tsx +48 -5
  79. package/src/client/config-manager.module.css +545 -34
  80. package/src/client/history/HistoryPanel.tsx +30 -29
  81. package/src/client/icon-layer-guard.test.ts +137 -0
  82. package/src/client/import/ImportWizardView.tsx +15 -4
  83. package/src/client/import/use-import-wizard-controller.ts +24 -7
  84. package/src/client/index.ts +4 -4
  85. package/src/client/locales.ts +118 -123
  86. package/src/client/lucide-icons.d.ts +11 -0
  87. package/src/client/market/MarketPanel.tsx +39 -37
  88. package/src/client/market/MyConfigsInstall.tsx +3 -2
  89. package/src/client/market/MyConfigsList.tsx +2 -1
  90. package/src/client/market/MyConfigsLoginCard.tsx +3 -2
  91. package/src/client/overview/OverviewPanel.tsx +4 -39
  92. package/src/client/profiles/ProfilesPanel.tsx +422 -215
  93. package/src/client/recovery/RecoveryPanel.tsx +120 -3
  94. package/src/client/recovery/incident-api.ts +91 -0
  95. package/src/client/recovery/recovery-locales.ts +56 -0
  96. package/src/client/recovery/recovery-view.ts +71 -0
  97. package/src/client/run-store.test.ts +174 -2
  98. package/src/client/run-store.ts +147 -28
  99. package/src/client/snapshots/BackupScheduleCard.tsx +32 -35
  100. package/src/client/snapshots/RestorePlanView.tsx +8 -5
  101. package/src/client/snapshots/SnapshotsPanel.tsx +13 -4
  102. package/src/client/sync/AutosyncCard.tsx +6 -10
  103. package/src/client/sync/ChannelConfigDialog.tsx +8 -10
  104. package/src/client/sync/SyncHistoryView.tsx +3 -2
  105. package/src/client/sync/SyncSettingsView.tsx +17 -18
  106. package/src/core/analyzer.ts +8 -21
  107. package/src/core/backup.ts +2 -2
  108. package/src/core/boot-paths.test.ts +35 -0
  109. package/src/core/boot-paths.ts +29 -0
  110. package/src/core/boot-rescue.test.ts +82 -0
  111. package/src/core/boot-rescue.ts +57 -1
  112. package/src/core/boot-safety.ts +2 -2
  113. package/src/core/conflict-decisions.ts +34 -0
  114. package/src/core/crash-report.test.ts +25 -41
  115. package/src/core/crash-report.ts +34 -41
  116. package/src/core/incident-wiring.test.ts +96 -0
  117. package/src/core/plugin-cli.fs.test.ts +52 -0
  118. package/src/core/plugin-cli.ts +20 -1
  119. package/src/core/smoke.test.ts +4 -1
  120. package/src/core/types.ts +9 -0
  121. package/src/index.ts +79 -163
  122. package/src/profiles/dsh-profile-launcher.test.ts +359 -0
  123. package/src/profiles/dsh-profile-launcher.ts +473 -0
  124. package/src/profiles/dsh-profile-manager.test.ts +123 -59
  125. package/src/profiles/dsh-profile-manager.ts +148 -51
  126. package/src/profiles/dsh-profile-runtime.test.ts +196 -0
  127. package/src/profiles/dsh-profile-runtime.ts +228 -0
  128. package/src/profiles/dsh-profile-shared.ts +126 -9
  129. package/src/profiles/index.ts +30 -4
  130. package/src/profiles/process-control.ts +99 -0
  131. package/src/routes/kit.ts +1 -0
  132. package/src/routes/profiles.ts +139 -18
  133. package/src/schema/registry.test.ts +36 -0
  134. package/src/schema/section-registry.ts +42 -2
  135. package/src/ui/dsh-profiles-view.test.ts +87 -12
  136. package/src/ui/dsh-profiles-view.ts +106 -17
  137. package/src/ui/i18n.test.ts +0 -2
  138. package/src/ui/i18n.ts +0 -2
  139. package/src/ui/import-wizard.test.ts +40 -2
  140. package/src/ui/import-wizard.ts +41 -7
  141. package/src/ui/select-model.test.ts +75 -0
  142. package/src/ui/select-model.ts +92 -0
  143. package/src/ui/selection-model.test.ts +47 -5
  144. package/src/ui/selection-model.ts +50 -3
  145. package/src/utils/env-lock.test.ts +120 -0
  146. package/src/utils/env-lock.ts +140 -21
  147. package/lib/core/config-lifecycle.d.ts +0 -179
  148. package/lib/core/config-lifecycle.js +0 -479
  149. package/lib/core/config-snapshot.d.ts +0 -137
  150. package/lib/core/config-snapshot.js +0 -443
  151. package/lib/core/config-state.d.ts +0 -70
  152. package/lib/core/config-state.js +0 -177
  153. package/lib/core/undo.d.ts +0 -73
  154. package/lib/core/undo.js +0 -87
  155. package/lib/core/watcher.d.ts +0 -101
  156. package/lib/core/watcher.js +0 -219
  157. package/src/client/lifecycle/LifecyclePanel.tsx +0 -525
  158. package/src/client/lifecycle/lifecycle-api.ts +0 -178
  159. package/src/core/config-lifecycle.test.ts +0 -980
  160. package/src/core/config-lifecycle.ts +0 -571
  161. package/src/core/config-snapshot.test.ts +0 -559
  162. package/src/core/config-snapshot.ts +0 -538
  163. package/src/core/config-state.test.ts +0 -381
  164. package/src/core/config-state.ts +0 -207
  165. package/src/core/phase1-wiring.test.ts +0 -148
  166. package/src/core/undo.ts +0 -116
  167. package/src/core/watcher.test.ts +0 -326
  168. package/src/core/watcher.ts +0 -263
package/README.md CHANGED
@@ -79,7 +79,7 @@ Browse the built-in official market for ready-made configurations (model provide
79
79
  | 🔄 | **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
80
  | ⏰ | **Scheduled backups** | Full backup on a fixed cadence (6h / 12h / 24h / 7d) — set-and-forget, secrets never included |
81
81
  | 🛒 | **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 |
82
+ | 🗂️ | **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
83
  | 🌐 | **Bilingual UI** | Interface, reports and error details follow the DSH app language (中文 / English) |
84
84
  | 🤖 | **Agent tools** | Backup / snapshot / restore / sync right from an agent session |
85
85
 
@@ -276,11 +276,18 @@ launched with `dsh --profile <name>`. This page reads and writes that directory
276
276
  | 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
277
  | Rename | Directory move + fixes the manifest name field; the running profile is refused |
278
278
  | 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>`).
279
+ | 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 |
280
+ | 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** |
281
+ | 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) |
282
+
283
+ > **Why there is no "set as next launch"** (button and marker removed in 2026-09): DSH has **no "default / next launch
284
+ > profile" state** — the profile comes only from the launch arguments (`dsh <name>` / `--profile <name>`; `dsh web` is a
285
+ > hard-coded alias), so any "which profile to use next" marker has **no consumer at all**: the `dsh web` you type
286
+ > yourself still boots web after a restart. Only two mechanisms really switch profiles: ① this page's "launch this
287
+ > profile" (an extra instance, no interruption, stoppable at any time), ② pointing your launch command/shortcut at
288
+ > `dsh --profile <name>` (ecosystem tools such as dshm and DSH Launcher all spawn instances from an external launcher).
289
+ > Instances started by this plugin are recorded in `<dataDir>/launches.json` (pid/port/log), which is why they can be
290
+ > stopped; third-party plugins must be installed into that profile separately (`dsh plugin --profile <name> add <pkg>`).
284
291
  > The tab lives at Settings → "Backup & Migration" → **Profiles**.
285
292
 
286
293
  ### 📸 Snapshot restore (undo an import)
@@ -540,6 +547,24 @@ Yes. The import wizard asks for the export-time encryption password and verifies
540
547
  5. **Encrypted backups**: a lost password means the `secrets.enc` can't be decrypted (by design — keep your password safe)
541
548
  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
549
 
550
+ ## 💬 Feedback
551
+
552
+ 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.
553
+
554
+ | What you want to say | Where |
555
+ |---|---|
556
+ | 🎨 **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 |
557
+ | 🐛 **Something is broken / an error / wrong data** | [Bug report](https://github.com/xiajiajun516/dsh-config-manager/issues/new?template=bug_report.en.yml) |
558
+ | ✨ **New feature idea** | [Feature request](https://github.com/xiajiajun516/dsh-config-manager/issues/new?template=feature_request.en.yml) |
559
+ | 💬 **Not sure whether it is a bug — just asking** | [Discussions](https://github.com/xiajiajun516/dsh-config-manager/discussions) |
560
+ | 🔒 **Security issue / leaked credential** | [Private security advisory](https://github.com/xiajiajun516/dsh-config-manager/security/advisories/new) (please do not open a public issue) |
561
+
562
+ **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.
563
+
564
+ **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.
565
+
566
+ > ⚠️ Please search for an existing issue first, and **strip every API key / token / password** — including the ones visible in screenshots and logs.
567
+
543
568
  ## 🙏 Contributors
544
569
 
545
570
  - **lux-liang (Jialiang Liang)** — [PR #44](https://github.com/xiajiajun516/dsh-config-manager/pull/44): independently fixed the
package/README.zh-CN.md CHANGED
@@ -13,7 +13,7 @@ DSH Config Manager 是一个 DeepSeek Harness 配置备份与迁移插件:一
13
13
  - Workspace / AGENTS.md
14
14
 
15
15
  > DSH 的 profile(`$DSH_HOME/profiles/<name>`)本身**不随备份迁移**(它是「用哪套插件组合启动」的机器本地选择);
16
- > 本插件的「档案」页可以列表 / 新建 / 重命名 / 删除它们,并记录「下次启动用哪个」。
16
+ > 本插件的「档案」页可以列表 / 新建 / 重命名 / 删除它们,也可以**用某个档案另起一个独立实例并随时停止它**(真正可用的切换)。
17
17
 
18
18
  把当前 DeepSeek Harness 环境导出为可移植备份,在另一台电脑上一键恢复;也支持通过 Git / WebDAV 跨机同步(密钥默认不同步,勾选「导出密钥」并加密后可随加密快照迁移);还能通过内置**配置市场**浏览、一键安装社区分享的现成配置。
19
19
 
@@ -81,7 +81,7 @@ DSH 是你的 AI 助手工作台,里面存着你的各种设置:模型配置
81
81
  | 🔄 | **远程同步** | 通过 **Git 私有仓库或 WebDAV** 推送 / 拉取可移植配置(密钥默认不参与同步;加密快照可选携带密文凭据) |
82
82
  | ⏰ | **定时全量备份** | 按固定周期(6h / 12h / 24h / 7d)自动全量备份,一劳永逸,密钥永不包含 |
83
83
  | 🛒 | **配置市场** | 浏览并一键安装社区分享的配置——供应链警示 + 逐项内容选择(可就地看到改动与高风险分区) |
84
- | 🗂️ | **档案 Profiles(DSH profile)** | 直接管理 `$DSH_HOME/profiles/<name>`:列表 / 新建(官方模板)/ 重命名 / 物理删除 / 记录「下次启动用哪个」 |
84
+ | 🗂️ | **档案 Profiles(DSH profile)** | 直接管理 `$DSH_HOME/profiles/<name>`:列表 / 新建(官方模板)/ 重命名 / 物理删除 / **启动该档案(独立实例)** / **停止实例**(行内按钮按运行状态在「启动 ↔ 停止」间切换) |
85
85
  | 🧩 | **本地插件随备份迁移** | `link:` / `file:` 安装的本地开发插件会被打包进备份,换机不再丢失 |
86
86
  | 🗄️ | **保留策略可配(GFS 分层)** | 「最近 N 份 + 每月留 1 份 + 每年留 1 份」,默认值等价旧行为 |
87
87
  | 🤖 | **Agent 工具** | Agent 会话内直接备份 / 快照 / 恢复 / 同步 |
@@ -272,10 +272,17 @@ dsh plugin --profile web add dsh-config-manager@latest
272
272
  | 新建 | 在 `$DSH_HOME/profiles/<name>` 写标准三件套(与官方 `initProfile` 等价);可选起步模板 base / web / headless / sdk / sdk-minimal / acp |
273
273
  | 重命名 | 目录级移动 + 同步修正 `package.json` 的 name;当前运行中的档案拒绝重命名 |
274
274
  | 删除 | **物理删除整个目录**(含 node_modules);当前运行中的档案需额外勾选确认 |
275
- | 下次启动 | 只写「下次启动用哪个」标记 + 给出 `dsh --profile <name>` 重启命令 |
276
-
277
- > DSH **无法在运行中切换 profile**(profile 由启动参数决定,bundle 层在启动时解析),因此本页不做进程操作,
278
- > 只记录选择并提示你手动重启。第三方插件需要在该档案里单独安装(`dsh plugin --profile <name> add <pkg>`)。
275
+ | 启动该档案 | **真正可用的切换**:以该档案另起一个**独立 DSH 实例**(自动挑空闲端口 + 自动打开浏览器),当前实例与正在跑的任务不受影响;只支持 **web 形态**档案:非 web 形态(如 base 模板)没有浏览器界面,点击后给出终端命令而不是静默失败 |
276
+ | 停止实例 | 该档案有实例在跑时,行内「启动」自动变成「停止」(运行状态卡里也有一个);先请它自己退出(优雅期),超时才结束进程树,并如实告诉你是哪种;**实例运行中拒绝物理删除该档案** |
277
+ | 不会重复启动 | 「哪些档案在跑」= 本插件启动的实例台账 ∪ **每个实例自报的心跳**(`<dataDir>/running/<profile>.json`,只含 pid/端口,**不含认证 token**)→ 哪怕某个档案是你手动 `dsh web` 起来的,本页也不会再启动第二个同名实例(明确提示已在运行);**当前这个实例本身**不给停止按钮(停自己会把你自己杀掉——请关窗口/终端,或到启动它的那个实例里停止) |
278
+
279
+ > **为什么没有「设为下次启动」**(2026-09 该按钮与标记一并移除):DSH **没有「默认 / 下次启动 profile」这种状态** ——
280
+ > profile 只由启动参数决定(`dsh <名>` / `--profile <名>`,`dsh web` 是硬编码别名),任何「下次启动用哪个」的标记
281
+ > **都没有消费者**:你自己敲的 `dsh web` 重启后当然还是 web。真正可用的切换只有两种:① 本页的「启动该档案」
282
+ > 另起一个实例(不打断当前会话,随时可停);② 把启动命令/快捷方式换成 `dsh --profile <名>`
283
+ > (生态里的 dshm、DSH Launcher 也都是「外部启动器 spawn 实例」这一条路)。
284
+ > 本插件启动的实例记在 `<dataDir>/launches.json`(pid/端口/日志),所以**关得掉**;
285
+ > 第三方插件需要在该档案里单独安装(`dsh plugin --profile <name> add <pkg>`)。
279
286
 
280
287
  ### 📸 快照恢复(撤销一次导入)
281
288
 
@@ -458,6 +465,24 @@ dsh-config-manager backup --sections skills,self # 收窄范围
458
465
  6. **快照恢复是离线的、诚实的**:离线引擎无法恢复的条目(快照无整文件备份时的 settings namespace / patch 行、存在 DSH storages 里的 workspace 记录)会如实列为跳过并指向在线回滚;凭据**值**绝不自动改写(只提示人工补录);无插件基线的旧快照只提示人工核对新增插件
459
466
  7. **本地源插件(`link:` / `file:`)随备份打包**:导出时执行 `npm pack` 把本地开发中的插件打成 tarball 一并备份,导入时解包到 `$DSH_HOME/dsh-config-manager/local-plugins/` 后按 `file:` 安装。因此:① 备份体积会随本地插件的体积增大(单插件超过 100 MB 会被跳过并告警,建议先发布到 registry / git 再备份);② 插件**源码**会进入备份(与「密钥永不进备份」不冲突——密钥仍被排除,这里进的是代码);③ 打包需要本机有可用的 `npm`,无 npm 时该插件退化为原行为(保留原 spec,换机后仍需手工安装)
460
467
 
468
+ ## 💬 反馈与建议
469
+
470
+ 遇到 Bug、界面错位、按钮点了没反应,或者只是有个想法 —— **都欢迎提出来**。界面问题尤其欢迎:这是最容易被自己忽略、也最该由真实使用场景决定的部分。
471
+
472
+ | 你想说什么 | 去哪儿 |
473
+ |---|---|
474
+ | 🎨 **界面问题**:布局错位、样式异常、深色模式、缩放、按钮无响应 | [UI 问题表单](https://github.com/xiajiajun516/dsh-config-manager/issues/new?template=ui_bug.yml)(只要截图 + 浏览器版本) |
475
+ | 🐛 **功能出错 / 报错 / 数据不对** | [Bug 报告](https://github.com/xiajiajun516/dsh-config-manager/issues/new?template=bug_report.yml) |
476
+ | ✨ **新功能建议** | [功能建议](https://github.com/xiajiajun516/dsh-config-manager/issues/new?template=feature_request.yml) |
477
+ | 💬 **不确定是不是 Bug,想先问问** | [Discussions](https://github.com/xiajiajun516/dsh-config-manager/discussions) |
478
+ | 🔒 **安全问题 / 密钥泄露** | [私密安全公告](https://github.com/xiajiajun516/dsh-config-manager/security/advisories/new)(请不要开公开 issue) |
479
+
480
+ **在插件里就地反馈**:设置 → 备份与迁移 → **关于** → 「反馈问题」;或点「关于」里的**复制环境信息**按钮,插件版本 / DSH 版本 / 平台会自动带上,粘进 issue 即可 —— 不用手抄版本号。
481
+
482
+ **每条都会跟进**:新 issue 会立刻收到一条回复并打上 `needs-triage`,处理进度体现在标签上(`needs-info` → `confirmed` → `fixed`)。修好的问题会出现在 [CHANGELOG.md](CHANGELOG.md) 的对应版本条目里,带 issue 编号(例:#38 / #43 / #45)—— 这就是一条反馈最终的落地记录。
483
+
484
+ > ⚠️ 提交前请先搜一下是否已有同类 issue,并**抹掉任何 API Key / Token / 密码**(截图和日志里也算)。
485
+
461
486
  ## 🙏 贡献者
462
487
 
463
488
  - **lux-liang (Jialiang Liang)** —— [PR #44](https://github.com/xiajiajun516/dsh-config-manager/pull/44):独立修复了 issue #43
@@ -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',