dsh-config-manager 0.1.60 → 0.1.62

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 (253) hide show
  1. package/README.md +25 -12
  2. package/README.zh-CN.md +27 -9
  3. package/lib/adapters/file-collection.d.ts +31 -2
  4. package/lib/adapters/file-collection.js +58 -9
  5. package/lib/adapters/plugin-files.d.ts +4 -2
  6. package/lib/adapters/plugin-files.js +13 -1
  7. package/lib/adapters/plugins.d.ts +11 -2
  8. package/lib/adapters/plugins.js +82 -10
  9. package/lib/adapters/self.d.ts +4 -1
  10. package/lib/adapters/self.js +10 -1
  11. package/lib/adapters/sessions.d.ts +24 -0
  12. package/lib/adapters/sessions.js +69 -0
  13. package/lib/adapters/test-helpers.d.ts +8 -0
  14. package/lib/adapters/test-helpers.js +15 -0
  15. package/lib/adapters/units.d.ts +32 -0
  16. package/lib/adapters/units.js +39 -0
  17. package/lib/adapters/workspaces.d.ts +4 -2
  18. package/lib/adapters/workspaces.js +15 -3
  19. package/lib/client.d.ts +683 -183
  20. package/lib/client.js +7761 -4503
  21. package/lib/core/analyzer.d.ts +10 -1
  22. package/lib/core/analyzer.js +50 -5
  23. package/lib/core/config-snapshot.js +1 -1
  24. package/lib/core/consult-source.d.ts +0 -21
  25. package/lib/core/consult-source.js +2 -46
  26. package/lib/core/exporter.js +22 -1
  27. package/lib/core/importer.d.ts +8 -2
  28. package/lib/core/importer.js +7 -3
  29. package/lib/core/index.d.ts +1 -0
  30. package/lib/core/index.js +2 -0
  31. package/lib/core/messages.d.ts +8 -0
  32. package/lib/core/messages.js +16 -0
  33. package/lib/core/migration-consult.d.ts +27 -3
  34. package/lib/core/migration-consult.js +102 -55
  35. package/lib/core/migration-history.d.ts +1 -1
  36. package/lib/core/migration-history.js +2 -2
  37. package/lib/core/restore.d.ts +6 -0
  38. package/lib/core/restore.js +4 -4
  39. package/lib/core/session-meta.d.ts +73 -0
  40. package/lib/core/session-meta.js +175 -0
  41. package/lib/core/session-select.d.ts +52 -0
  42. package/lib/core/session-select.js +90 -0
  43. package/lib/core/snapshot-diff.d.ts +106 -0
  44. package/lib/core/snapshot-diff.js +309 -0
  45. package/lib/core/types.d.ts +121 -1
  46. package/lib/index.d.ts +72 -3
  47. package/lib/index.js +445 -228
  48. package/lib/market/market-config.js +2 -1
  49. package/lib/market/reader.js +1 -1
  50. package/lib/market/security.js +2 -1
  51. package/lib/market/types.d.ts +1 -1
  52. package/lib/market/view.d.ts +1 -1
  53. package/lib/profiles/dsh-profile-manager.d.ts +59 -0
  54. package/lib/profiles/dsh-profile-manager.js +305 -0
  55. package/lib/profiles/dsh-profile-shared.d.ts +85 -0
  56. package/lib/profiles/dsh-profile-shared.js +50 -0
  57. package/lib/profiles/index.d.ts +8 -2
  58. package/lib/profiles/index.js +8 -2
  59. package/lib/security/credentials-yaml.d.ts +26 -0
  60. package/lib/security/credentials-yaml.js +47 -0
  61. package/lib/security/index.d.ts +1 -0
  62. package/lib/security/index.js +1 -0
  63. package/lib/sync/backup-scheduler.d.ts +17 -4
  64. package/lib/sync/backup-scheduler.js +28 -15
  65. package/lib/sync/layout.js +5 -0
  66. package/lib/sync/snapshot-crypto.d.ts +17 -1
  67. package/lib/sync/snapshot-crypto.js +40 -1
  68. package/lib/sync/snapshot-json.js +7 -1
  69. package/lib/sync/sync-engine.d.ts +69 -4
  70. package/lib/sync/sync-engine.js +150 -18
  71. package/lib/sync/sync-selection.d.ts +19 -1
  72. package/lib/sync/sync-selection.js +28 -4
  73. package/lib/sync/sync-session.d.ts +8 -0
  74. package/lib/sync/transport.d.ts +18 -0
  75. package/lib/sync/transport.js +7 -0
  76. package/lib/ui/backup-schedule.d.ts +22 -0
  77. package/lib/ui/backup-schedule.js +28 -0
  78. package/lib/ui/diff-view.d.ts +34 -0
  79. package/lib/ui/diff-view.js +66 -0
  80. package/lib/ui/dsh-profiles-view.d.ts +63 -0
  81. package/lib/ui/dsh-profiles-view.js +110 -0
  82. package/lib/ui/export-flow.d.ts +35 -6
  83. package/lib/ui/export-flow.js +32 -19
  84. package/lib/ui/history-model.d.ts +5 -0
  85. package/lib/ui/history-model.js +13 -2
  86. package/lib/ui/i18n.d.ts +27 -0
  87. package/lib/ui/i18n.js +56 -0
  88. package/lib/ui/import-wizard.d.ts +16 -0
  89. package/lib/ui/import-wizard.js +14 -0
  90. package/lib/ui/market-import.d.ts +106 -0
  91. package/lib/ui/market-import.js +167 -0
  92. package/lib/ui/migration-consult-view.d.ts +3 -0
  93. package/lib/ui/migration-consult-view.js +2 -0
  94. package/lib/ui/nav-overflow.d.ts +35 -0
  95. package/lib/ui/nav-overflow.js +41 -0
  96. package/lib/ui/overview-view.d.ts +4 -9
  97. package/lib/ui/overview-view.js +11 -19
  98. package/lib/ui/report.d.ts +5 -0
  99. package/lib/ui/report.js +36 -0
  100. package/lib/ui/restore-plan-view.d.ts +74 -0
  101. package/lib/ui/restore-plan-view.js +155 -0
  102. package/lib/ui/selection-model.d.ts +227 -0
  103. package/lib/ui/selection-model.js +471 -0
  104. package/lib/ui/types.d.ts +7 -2
  105. package/lib/utils/line-diff.d.ts +62 -0
  106. package/lib/utils/line-diff.js +260 -0
  107. package/lib/utils/logger.d.ts +9 -0
  108. package/lib/utils/logger.js +12 -0
  109. package/package.json +1 -1
  110. package/src/adapters/file-collection.ts +63 -10
  111. package/src/adapters/plugin-files.ts +13 -2
  112. package/src/adapters/plugins.ts +84 -11
  113. package/src/adapters/self.ts +10 -1
  114. package/src/adapters/sessions.test.ts +108 -0
  115. package/src/adapters/sessions.ts +75 -0
  116. package/src/adapters/test-helpers.ts +13 -0
  117. package/src/adapters/units.test.ts +228 -0
  118. package/src/adapters/units.ts +63 -0
  119. package/src/adapters/workspaces.ts +17 -4
  120. package/src/client/ConfigManagerSection.tsx +35 -3
  121. package/src/client/about/ReleaseNotesDialog.tsx +4 -1
  122. package/src/client/api.ts +121 -57
  123. package/src/client/common/ContentPicker.tsx +334 -0
  124. package/src/client/common/ErrorBanner.tsx +10 -3
  125. package/src/client/common/Modal.tsx +17 -5
  126. package/src/client/common/ProgressBar.tsx +8 -4
  127. package/src/client/common/ReportView.tsx +126 -17
  128. package/src/client/common/SectionComposition.tsx +50 -11
  129. package/src/client/common/composition-summary.test.ts +63 -0
  130. package/src/client/common/modal-close-label.test.ts +80 -0
  131. package/src/client/common/plan-text-redaction.test.ts +283 -0
  132. package/src/client/common/section-labels.test.ts +114 -0
  133. package/src/client/common/section-labels.ts +51 -0
  134. package/src/client/config-manager.module.css +657 -4
  135. package/src/client/consult/ConsultCard.tsx +8 -0
  136. package/src/client/export/ExportView.tsx +293 -128
  137. package/src/client/history/HistoryPanel.tsx +17 -6
  138. package/src/client/history/history-locales.ts +11 -2
  139. package/src/client/import/ConflictList.tsx +27 -9
  140. package/src/client/import/ImportWizardView.tsx +148 -28
  141. package/src/client/import/PathMappingForm.tsx +5 -2
  142. package/src/client/lifecycle/LifecyclePanel.tsx +2 -1
  143. package/src/client/locales.ts +359 -223
  144. package/src/client/market/MarketImportReview.tsx +415 -0
  145. package/src/client/market/MarketPanel.tsx +101 -139
  146. package/src/client/market/MyConfigsView.tsx +78 -119
  147. package/src/client/market/market-api.ts +4 -2
  148. package/src/client/market/market-locales.ts +111 -17
  149. package/src/client/market/market-view.test.ts +11 -72
  150. package/src/client/market/market-view.ts +12 -146
  151. package/src/client/market/my-configs-view.ts +22 -13
  152. package/src/client/overview/OverviewPanel.tsx +25 -5
  153. package/src/client/overview/overview-kind-labels.test.ts +43 -0
  154. package/src/client/profiles/ProfilesPanel.tsx +384 -390
  155. package/src/client/run-store.test.ts +21 -9
  156. package/src/client/run-store.ts +116 -38
  157. package/src/client/snapshots/RestorePlanView.tsx +314 -0
  158. package/src/client/snapshots/SnapshotsPanel.tsx +47 -47
  159. package/src/client/snapshots/report-dismiss.test.ts +59 -0
  160. package/src/client/sync/SyncConfirmView.tsx +8 -3
  161. package/src/client/sync/SyncSettingsView.tsx +263 -122
  162. package/src/client/sync/sync-api.ts +34 -2
  163. package/src/client/sync/sync-locales.ts +46 -28
  164. package/src/client/sync/sync-push-preview.test.ts +10 -1
  165. package/src/client/sync/sync-view.test.ts +38 -4
  166. package/src/client/sync/sync-view.ts +63 -9
  167. package/src/core/analyzer.ts +50 -4
  168. package/src/core/config-snapshot.ts +1 -1
  169. package/src/core/consult-source.test.ts +2 -16
  170. package/src/core/consult-source.ts +3 -58
  171. package/src/core/exporter.ts +20 -1
  172. package/src/core/importer.ts +10 -3
  173. package/src/core/index.ts +6 -0
  174. package/src/core/messages.ts +16 -0
  175. package/src/core/migration-consult.test.ts +44 -5
  176. package/src/core/migration-consult.ts +115 -55
  177. package/src/core/migration-history.test.ts +2 -2
  178. package/src/core/migration-history.ts +2 -2
  179. package/src/core/restore.ts +4 -4
  180. package/src/core/session-meta.test.ts +172 -0
  181. package/src/core/session-meta.ts +212 -0
  182. package/src/core/session-select.test.ts +86 -0
  183. package/src/core/session-select.ts +103 -0
  184. package/src/core/snapshot-diff.test.ts +241 -0
  185. package/src/core/snapshot-diff.ts +418 -0
  186. package/src/core/types.ts +117 -1
  187. package/src/index.sync.test.ts +31 -1
  188. package/src/index.ts +454 -229
  189. package/src/market/market-config.ts +2 -1
  190. package/src/market/market.test.ts +2 -2
  191. package/src/market/reader.ts +1 -1
  192. package/src/market/security.ts +2 -1
  193. package/src/market/types.ts +1 -1
  194. package/src/market/view.ts +1 -1
  195. package/src/profiles/dsh-profile-manager.test.ts +271 -0
  196. package/src/profiles/dsh-profile-manager.ts +313 -0
  197. package/src/profiles/dsh-profile-shared.ts +120 -0
  198. package/src/profiles/index.ts +18 -5
  199. package/src/security/credentials-yaml.test.ts +111 -0
  200. package/src/security/credentials-yaml.ts +44 -0
  201. package/src/security/index.ts +1 -0
  202. package/src/sync/backup-scheduler.test.ts +73 -0
  203. package/src/sync/backup-scheduler.ts +34 -17
  204. package/src/sync/layout.ts +5 -0
  205. package/src/sync/snapshot-crypto.ts +48 -2
  206. package/src/sync/snapshot-json.ts +9 -2
  207. package/src/sync/sync-credentials.test.ts +334 -0
  208. package/src/sync/sync-engine.test.ts +102 -0
  209. package/src/sync/sync-engine.ts +199 -22
  210. package/src/sync/sync-selection.test.ts +61 -7
  211. package/src/sync/sync-selection.ts +37 -5
  212. package/src/sync/sync-session.ts +8 -0
  213. package/src/sync/transport.ts +24 -0
  214. package/src/ui/backup-schedule.test.ts +60 -0
  215. package/src/ui/backup-schedule.ts +40 -0
  216. package/src/ui/diff-view.test.ts +66 -0
  217. package/src/ui/diff-view.ts +91 -0
  218. package/src/ui/dsh-profiles-view.test.ts +117 -0
  219. package/src/ui/dsh-profiles-view.ts +142 -0
  220. package/src/ui/export-flow.test.ts +33 -11
  221. package/src/ui/export-flow.ts +56 -20
  222. package/src/ui/history-model.test.ts +15 -2
  223. package/src/ui/history-model.ts +12 -2
  224. package/src/ui/i18n.ts +56 -0
  225. package/src/ui/import-wizard.test.ts +27 -1
  226. package/src/ui/import-wizard.ts +28 -1
  227. package/src/ui/market-import.test.ts +201 -0
  228. package/src/ui/market-import.ts +263 -0
  229. package/src/ui/migration-consult-view.test.ts +2 -0
  230. package/src/ui/migration-consult-view.ts +5 -0
  231. package/src/ui/nav-overflow.test.ts +57 -0
  232. package/src/ui/nav-overflow.ts +58 -0
  233. package/src/ui/overview-view.test.ts +1 -19
  234. package/src/ui/overview-view.ts +12 -29
  235. package/src/ui/report.test.ts +10 -1
  236. package/src/ui/report.ts +38 -1
  237. package/src/ui/restore-plan-view.test.ts +121 -0
  238. package/src/ui/restore-plan-view.ts +222 -0
  239. package/src/ui/selection-model.test.ts +503 -0
  240. package/src/ui/selection-model.ts +599 -0
  241. package/src/ui/types.ts +5 -3
  242. package/src/utils/line-diff.test.ts +157 -0
  243. package/src/utils/line-diff.ts +287 -0
  244. package/src/utils/logger.test.ts +104 -0
  245. package/src/utils/logger.ts +14 -0
  246. package/lib/profiles/profile-manager.d.ts +0 -106
  247. package/lib/profiles/profile-manager.js +0 -543
  248. package/lib/ui/profiles-view.d.ts +0 -26
  249. package/lib/ui/profiles-view.js +0 -38
  250. package/src/profiles/profile-manager.test.ts +0 -282
  251. package/src/profiles/profile-manager.ts +0 -630
  252. package/src/ui/profiles-view.test.ts +0 -77
  253. package/src/ui/profiles-view.ts +0 -55
package/README.md CHANGED
@@ -40,7 +40,7 @@ DSH is your AI assistant workbench — it holds your settings: model configs, pl
40
40
 
41
41
  ### Backup DeepSeek Harness configuration
42
42
 
43
- Create a portable backup of your DSH settings, model providers, plugins, MCP servers, skills, agent presets, profiles and workspace — one ZIP file, no secret values included by default.
43
+ Create a portable backup of your DSH settings, model providers, plugins, MCP servers, skills, agent presets and workspace — one ZIP file, no secret values included by default. (DSH's own profiles — `$DSH_HOME/profiles/<name>`, i.e. which plugin stack to boot — are machine-local and are not migrated; the **Profiles** page can list/create/rename/delete them and record which one the next launch should use.)
44
44
 
45
45
  ### Restore DeepSeek Harness on another machine
46
46
 
@@ -52,7 +52,7 @@ Move your complete DeepSeek Harness setup without manually reinstalling plugins,
52
52
 
53
53
  ### Sync DSH configuration across machines
54
54
 
55
- Keep portable configuration synchronized between machines through a private Git repository or WebDAV — secrets never sync.
55
+ Keep portable configuration synchronized between machines through a private Git repository or WebDAV — secrets do not sync by default (the payload is run through the SecretScanner); check "Export secrets" with an encryption password and `~/.dsh/.credentials.yaml` travels as scrypt + AES-256-GCM ciphertext inside the encrypted snapshot, so another machine can restore the credentials while the remote (Git host / WebDAV provider) only ever sees ciphertext.
56
56
 
57
57
  ### Schedule automatic full backups
58
58
 
@@ -76,10 +76,10 @@ Browse the built-in official market for ready-made configurations (model provide
76
76
  | 🔒 | **Secret safety** | API Keys are not exported by default — non-encrypted imports ask you to re-enter; encrypted backups restore them with the password |
77
77
  | ↩️ | **Automatic rollback** | Failed import restores everything automatically |
78
78
  | 📸 | **Snapshot restore** | Undo an import: whole-file restore + uninstall added plugins (CLI & GUI) |
79
- | 🔄 | **Remote Sync** | Push/pull portable config via **Git private repo or WebDAV** (secrets never sync) |
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
- | 🛒 | **Config Marketplace** | Browse & one-click install community configs — supply-chain warnings + per-section approval |
82
- | 🗂️ | **Profiles** | Save multiple setups (Work / Personal) and switch anytime — preview + auto-backup + rollback |
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 |
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
 
@@ -248,6 +248,7 @@ Push / pull your portable config between machines through **either of two channe
248
248
  - **WebDAV auth** uses HTTP Basic: the `username` is stored in the config and may be echoed back into the UI, while the `password` is read live from the DSH credentials slot `DSH_CONFIG_MANAGER_SYNC_WEBDAV_PASSWORD` — it never appears in any sync file or log.
249
249
  - **Plugins auto-install**: when pulling diffs, plugins that are new in the backup are **installed automatically** on confirm — no manual per-item ticking in the diff list. Only **version-conflict** plugins still ask you to pick "Keep Current / Use Imported".
250
250
  - **Push preview before uploading** — the Push button first shows a read-only preview of what will be sent (sections + per-section counts + changed-vs-baseline markers, first-baseline notice) and only writes the remote after you confirm.
251
+ - **Secrets do not sync by default**: every section goes through the `SecretScanner` (sensitive field values stripped) and the credential section is structurally excluded. With "Export secrets" checked and an encryption password set, `~/.dsh/.credentials.yaml` travels as scrypt + AES-256-GCM ciphertext in a **separate credentials payload** of the encrypted snapshot (never inside any section); the receiving side decrypts it into per-ref "credential migration" items that are written back to the local credential store (`credentials.set`) only after you confirm. The encryption/decryption password is kept in a dedicated local DSH credential slot (`~/.dsh/.credentials.yaml`): it is remembered once "Encrypt backup" is checked (leave the fields empty to reuse it) and cleared when you uncheck it or press "Delete saved password" — never written to sync files, responses, logs or the exported backup, and never sent back to the browser. The decryption password is only ever used when the pulled snapshot is actually encrypted. **Auto sync never carries credentials** (it has no password and skips encrypted snapshots).
251
252
 
252
253
  ### 🛒 Configuration Marketplace
253
254
 
@@ -256,18 +257,27 @@ Browse and install ready-made configurations (model providers, plugins, MCP serv
256
257
  - **Built-in official market** — read-only, bound to the official public repo (official badge shown, not editable); first open auto-refreshes, manual refresh also available
257
258
  - **Search & filter** — keyword search (matches name / description / author / **categories**), category filter, **section filter** (items already downloaded list their sections; others are excluded with a hint), source filter (Official / Community), sorting (recently updated / most starred / name A–Z), and a ⭐ badge showing the **source repo's** star count (queried anonymously, no token involved)
258
259
  - **Impact preview** — the detail view shows "what installing this will change" (items updated / identical / conflicts / secrets to re-enter / DSH restart needed) before you approve anything
259
- - **Supply-chain warnings always shown** — source repo URL, "not officially reviewed", download time; **per-section approval** — high-risk sections (sessions / arbitrary files) are banned from listing outright, and every remaining section must be explicitly approved before the import is confirmed
260
+ - **Supply-chain warnings always shown** — source repo URL, "not officially reviewed", download time; **per-item content selection** — the selection step lists what each item will change and flags high-risk sections in place (selected by default; unchecking excludes them from the import and the snapshot); high-risk content (sessions / arbitrary files) is still banned from listing outright
260
261
  - **Install reuses the safe import pipeline** — analyze → preview → auto-backup → apply → rollback; nothing is written before you confirm
261
262
  - **"My Configs"** — sign in with GitHub (device flow), upload a config to **your own public repo** in one click, and an **auto listing PR** is opened against the official market repo; manage your listings (status badges: not listed / PR pending / listed), update in one click, install back locally, or delist (auto de-listing PR)
262
263
 
263
- ### 🗂️ Profiles
264
+ ### 🗂️ Profiles (= DSH's own profiles)
264
265
 
265
- Save multiple configurations (Work / Personal) and switch anytime; switching includes preview + auto-backup + rollback.
266
+ A "profile" here is DSH's own profile (`$DSH_HOME/profiles/<name>`) — one **plugin stack (bundles) + dependencies + patch layer**,
267
+ launched with `dsh --profile <name>`. This page reads and writes that directory directly instead of keeping its own config snapshots:
266
268
 
267
- - **Save current config** — pick a name and store the current DSH configuration (settings / providers / plugins / skills / agent presets…; no secrets, file sections embedded)
268
- - **Switch with safety** — preview first (zero writes) → confirm → automatic snapshot → staged apply; any failure rolls back fully
269
- - **Manage** — rename / delete (confirmed) / import an exported profile.json
270
- - Library is an independent tab: Settings → "Backup & Migration" → **Profiles**
269
+ | Action | What it does |
270
+ |---|---|
271
+ | List / details | Bundle layers, dependencies, patch entries and size, patchReload, node_modules, mtime; details show the raw `package.json` and `cordis.patch.yml` (redacted before display) |
272
+ | 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 |
273
+ | Rename | Directory move + fixes the manifest name field; the running profile is refused |
274
+ | Delete | **Hard-deletes the whole directory** (including node_modules); deleting the running profile needs an extra checkbox |
275
+ | Next launch | Only records which profile to use next and shows the `dsh --profile <name>` restart command |
276
+
277
+ > DSH **cannot switch profiles while running** (the profile comes from the launch flag and bundle layers resolve at boot),
278
+ > so this page never touches processes — it records your choice and asks you to restart.
279
+ > Third-party plugins must be installed into that profile separately (`dsh plugin --profile <name> add <pkg>`).
280
+ > The tab lives at Settings → "Backup & Migration" → **Profiles**.
271
281
 
272
282
  ### 📸 Snapshot restore (undo an import)
273
283
 
@@ -488,6 +498,9 @@ No. A checksum mismatch rejects the import outright (protects against corruption
488
498
  **Q: Will re-importing duplicate things?**
489
499
  No. Items are deduplicated by stable IDs (plugin ID / MCP name / skill name…); existing items are skipped.
490
500
 
501
+ **Q: Why is the console quiet after `dsh web` — how do I get the plugin logs back?**
502
+ By design. Routine progress logs (mount banner, scheduler skips, export/backup completion) are emitted at `info`, and the shipped default level is `warn` — so only warnings and errors reach the terminal. Set `DSH_CONFIG_MANAGER_LOG_LEVEL=info` (or `debug`) before starting DSH to bring the verbose lines back.
503
+
491
504
  **Q: Does importing an encrypted backup require the password?**
492
505
  Yes. The import wizard asks for the export-time encryption password and verifies it before the import can proceed; the password is never saved — memory only. A wrong or missing password blocks the import (credentials are restored from the backup instead of being re-entered when the password is correct).
493
506
 
package/README.zh-CN.md CHANGED
@@ -10,10 +10,12 @@ DSH Config Manager 是一个 DeepSeek Harness 配置备份与迁移插件:一
10
10
  - MCP Servers
11
11
  - Skills
12
12
  - Agent Presets
13
- - Profiles(配置档案)
14
13
  - Workspace / AGENTS.md
15
14
 
16
- 把当前 DeepSeek Harness 环境导出为可移植备份,在另一台电脑上一键恢复;也支持通过 Git / WebDAV 跨机同步(密钥永不参与同步);还能通过内置**配置市场**浏览、一键安装社区分享的现成配置。
15
+ > DSH 的 profile(`$DSH_HOME/profiles/<name>`)本身**不随备份迁移**(它是「用哪套插件组合启动」的机器本地选择);
16
+ > 本插件的「档案」页可以列表 / 新建 / 重命名 / 删除它们,并记录「下次启动用哪个」。
17
+
18
+ 把当前 DeepSeek Harness 环境导出为可移植备份,在另一台电脑上一键恢复;也支持通过 Git / WebDAV 跨机同步(密钥默认不同步,勾选「导出密钥」并加密后可随加密快照迁移);还能通过内置**配置市场**浏览、一键安装社区分享的现成配置。
17
19
 
18
20
  [English](README.md) · [简体中文](README.zh-CN.md)
19
21
 
@@ -52,7 +54,7 @@ DSH 是你的 AI 助手工作台,里面存着你的各种设置:模型配置
52
54
 
53
55
  ### 在多台电脑间同步 DSH 配置
54
56
 
55
- 通过私有 Git 仓库或 WebDAV 在多台电脑间保持可移植配置同步——密钥永不参与同步。
57
+ 通过私有 Git 仓库或 WebDAV 在多台电脑间保持可移植配置同步——密钥默认不参与同步(同步载荷会被 SecretScanner 剥离);勾选「导出密钥」并设置加密密码后,`~/.dsh/.credentials.yaml` 会以 scrypt + AES-256-GCM 密文随加密快照上行,换机后可随同步写回本机凭据,远端(Git 仓库 / WebDAV 服务商)全程只见密文。
56
58
 
57
59
  ### 定时自动全量备份
58
60
 
@@ -76,10 +78,10 @@ DSH 是你的 AI 助手工作台,里面存着你的各种设置:模型配置
76
78
  | 🔒 | **密钥安全** | API Key 默认不导出;非加密导入后提醒重新填写,加密备份用密码解锁恢复 |
77
79
  | ↩️ | **自动回滚** | 导入失败自动恢复原样,不会弄坏现有配置 |
78
80
  | 📸 | **快照恢复** | 撤销一次导入:整文件还原 + 卸载新增插件(CLI 与 GUI 均支持) |
79
- | 🔄 | **远程同步** | 通过 **Git 私有仓库或 WebDAV** 推送 / 拉取可移植配置(密钥永不参与同步) |
81
+ | 🔄 | **远程同步** | 通过 **Git 私有仓库或 WebDAV** 推送 / 拉取可移植配置(密钥默认不参与同步;加密快照可选携带密文凭据) |
80
82
  | ⏰ | **定时全量备份** | 按固定周期(6h / 12h / 24h / 7d)自动全量备份,一劳永逸,密钥永不包含 |
81
- | 🛒 | **配置市场** | 浏览并一键安装社区分享的配置——供应链警示 + 逐分区批准 |
82
- | 🗂️ | **配置档案 Profiles** | 保存多套配置(工作 / 个人),随时切换 |
83
+ | 🛒 | **配置市场** | 浏览并一键安装社区分享的配置——供应链警示 + 逐项内容选择(可就地看到改动与高风险分区) |
84
+ | 🗂️ | **档案 Profiles(DSH profile)** | 直接管理 `$DSH_HOME/profiles/<name>`:列表 / 新建(官方模板)/ 重命名 / 物理删除 / 记录「下次启动用哪个」 |
83
85
  | 🧩 | **本地插件随备份迁移** | `link:` / `file:` 安装的本地开发插件会被打包进备份,换机不再丢失 |
84
86
  | 🗄️ | **保留策略可配(GFS 分层)** | 「最近 N 份 + 每月留 1 份 + 每年留 1 份」,默认值等价旧行为 |
85
87
  | 🤖 | **Agent 工具** | Agent 会话内直接备份 / 快照 / 恢复 / 同步 |
@@ -243,6 +245,7 @@ dsh plugin --profile web add dsh-config-manager@latest
243
245
  - **切换通道重新开始**:Git 与 WebDAV 的快照 / 共同祖先**互不共享**。切换通道后,同步从新远端的空基线重新开始——请先推送一个新快照。
244
246
  - **WebDAV 认证**采用 HTTP Basic:`username` 存配置、可在界面回显;`password` 则实时从 DSH credentials 槽位 `DSH_CONFIG_MANAGER_SYNC_WEBDAV_PASSWORD` 读取——绝不出现于任何同步文件或日志。
245
247
  - **插件自动安装**:拉取差异时,备份里新增的插件会在确认导入时**自动安装**,无需在差异列表里逐项手动勾选;只有**版本冲突**的插件仍需要你决定「保留当前 / 采用备份」。
248
+ - **密钥默认不同步**:同步载荷逐分区过 `SecretScanner`(敏感字段值剥离),凭据分区结构性排除。勾选「导出密钥」并设置加密密码后,`~/.dsh/.credentials.yaml` 以 scrypt + AES-256-GCM 密文作为**独立凭据载荷**随加密快照上行(不放进任何分区);拉取侧解密后生成逐条「凭据迁移」项,你确认采纳才写回本机凭据(`credentials.set`)。加密/解密密码保存在本机 DSH 凭据库(`~/.dsh/.credentials.yaml` 的独立槽位):勾选「加密备份」后自动记住(输入框留空即沿用),取消勾选或点「删除已保存密码」即清除 —— 密码绝不写入同步文件、响应、日志或导出备份,也绝不回传浏览器;解密密码只在拉取的快照确实加密时才会被使用。**自动同步恒不含凭据**(无密码可用,遇到加密快照会跳过)。
246
249
 
247
250
  ### 🛒 配置市场(Marketplace)
248
251
 
@@ -250,13 +253,25 @@ dsh plugin --profile web add dsh-config-manager@latest
250
253
 
251
254
  - **内置官方市场**——只读、绑定官方公开仓库(官方徽章,不可编辑);首次打开自动刷新,也支持手动拉取最新
252
255
  - **搜索与筛选**——关键词搜索、类别过滤、来源筛选(官方 / 个人)、排序(最近更新 / ⭐ 最多 / 名称 A–Z),并显示**来源仓库**的 ⭐ star 数(匿名查询,不触碰任何 token)
253
- - **供应链警示恒展示**——来源仓库 URL +「未经官方审核」+ 下载时间;**逐分区批准**——高风险分区(历史会话 / 任意文件)直接禁止上架,其余分区确认导入前须逐项显式勾选
256
+ - **供应链警示恒展示**——来源仓库 URL +「未经官方审核」+ 下载时间;**逐项内容选择**——选择页列出每个单元「本次将改动什么」并就地标出高风险分区(默认勾选,取消勾选即不导入、也不进快照);历史会话 / 任意文件类高风险内容仍直接禁止上架
254
257
  - **安装复用安全导入管道**——分析 → 预览 → 自动备份 → 执行 → 回滚;确认前零写入
255
258
  - **「我的配置」**——GitHub 登录(device flow)后一键上传到**你自己的公开仓库**,并自动向官方市场仓库提交**收录 PR**;管理已上传条目(收录状态徽章:未收录 / PR 待审核 / 已收录)、一键更新、装回本地、删除(自动下架 PR)
256
259
 
257
- ### 🗂️ 配置档案(Profiles)
260
+ ### 🗂️ 档案(Profiles = DSH 自带 profile)
261
+
262
+ 「档案」就是 DSH 自己的 profile(`$DSH_HOME/profiles/<name>`)——一套**插件组合(bundles)+ 依赖 + patch 层**,
263
+ 用 `dsh --profile <name>` 启动。本页直接读写这个目录,不再自建一套「配置快照」:
264
+
265
+ | 动作 | 说明 |
266
+ |---|---|
267
+ | 列表 / 详情 | bundles 层、依赖、patch 条目与体积、patchReload、node_modules、更新时间;详情可看 `package.json` 与 `cordis.patch.yml` 原文(展示前脱敏) |
268
+ | 新建 | 在 `$DSH_HOME/profiles/<name>` 写标准三件套(与官方 `initProfile` 等价);可选起步模板 base / web / headless / sdk / sdk-minimal / acp |
269
+ | 重命名 | 目录级移动 + 同步修正 `package.json` 的 name;当前运行中的档案拒绝重命名 |
270
+ | 删除 | **物理删除整个目录**(含 node_modules);当前运行中的档案需额外勾选确认 |
271
+ | 下次启动 | 只写「下次启动用哪个」标记 + 给出 `dsh --profile <name>` 重启命令 |
258
272
 
259
- 保存多套配置(如「工作」「个人」),随时切换;切换同样带预览 + 自动备份 + 回滚。
273
+ > DSH **无法在运行中切换 profile**(profile 由启动参数决定,bundle 层在启动时解析),因此本页不做进程操作,
274
+ > 只记录选择并提示你手动重启。第三方插件需要在该档案里单独安装(`dsh plugin --profile <name> add <pkg>`)。
260
275
 
261
276
  ### 📸 快照恢复(撤销一次导入)
262
277
 
@@ -424,6 +439,9 @@ dsh-config-manager backup --sections skills,self # 收窄范围
424
439
  **Q:加密备份导入时需要密码吗?**
425
440
  需要。导入向导会要求输入导出时设置的加密密码并验证通过后才能继续执行;密码仅存于内存、绝不保存。密码错误或缺失都会阻止导入(密码正确时凭据直接从备份解密恢复,无需重新填写)。
426
441
 
442
+ **Q:`dsh web` 启动后控制台为什么安静了?想恢复日志怎么办?**
443
+ 这是刻意的。常规进度日志(挂载横幅、调度器跳过、导出/备份完成)是 `info` 级别,而插件出厂默认级别为 `warn`——所以控制台只剩警告与错误。启动 DSH 前设 `DSH_CONFIG_MANAGER_LOG_LEVEL=info`(或 `debug`)即可恢复逐条输出。
444
+
427
445
  ---
428
446
 
429
447
  ## 📋 已知限制(用户须知)
@@ -1,6 +1,6 @@
1
1
  import type { MsgFunc } from '../core/messages.ts';
2
2
  import type { FilesSection, SectionId } from '../schema/types.ts';
3
- import type { ApplyResult, ConfigAdapter, ExportOptions, ExportSection, HostContext, ImportContext, PlanItem, Portability, ValidationResult } from '../core/types.ts';
3
+ import type { ApplyResult, ConfigAdapter, ExportOptions, ExportSection, ExportUnit, HostContext, ImportContext, PlanItem, Portability, ValidationResult } from '../core/types.ts';
4
4
  export declare abstract class FileCollectionAdapter implements ConfigAdapter<FilesSection> {
5
5
  abstract readonly id: SectionId;
6
6
  abstract readonly displayName: string;
@@ -8,7 +8,36 @@ export declare abstract class FileCollectionAdapter implements ConfigAdapter<Fil
8
8
  abstract readonly portability: Portability;
9
9
  /** 相对 homeDir 的基准目录(与 core/backup.ts FILE_BASES 一致) */
10
10
  abstract readonly baseDir: string;
11
- export(ctx: HostContext, _options: ExportOptions): Promise<ExportSection<FilesSection>>;
11
+ /**
12
+ * 最小可拆单元的 id(相对 baseDir 的路径)。
13
+ *
14
+ * 缺省 = **首个路径段** —— 目录 bundle 整体成一个单元,这是技能/会话/预设的正确粒度
15
+ * (拆开即失效)。平铺文件即自身。pluginFiles / self 覆写为「整条相对路径」:
16
+ * 它们的白名单文件彼此独立,不构成 bundle。
17
+ */
18
+ protected unitIdOf(relativePath: string): string;
19
+ /** 清单里的路径 → 相对 baseDir 的路径(baseDir 为空表示整目录即根)。 */
20
+ protected relPathOf(rel: string): string;
21
+ /**
22
+ * 条目级选择(Phase 1):只导出白名单命中的单元。
23
+ *
24
+ * 过滤发生在 readFile **之前** —— 未勾选的文件不读盘,大分区(sessions)取消勾选后
25
+ * 导出耗时应显著下降。allow === undefined(键缺省)= 全量,与改造前完全一致。
26
+ */
27
+ /**
28
+ * 单元级预筛选钩子(缺省 null = 不筛选)。
29
+ *
30
+ * 子类可据 `options` 与清单计算「允许导出的单元」子集(如 sessions 的「最新 N 个」,
31
+ * 见 core/session-select.ts);`keep: null` = 本次不施加单元级筛选(但仍可带告警)。
32
+ * 在 readFile **之前**执行:未入选的文件不读盘。
33
+ */
34
+ protected restrictUnits(_ctx: HostContext, _rels: readonly string[], _options: ExportOptions): Promise<{
35
+ keep: Set<string> | null;
36
+ warnings: string[];
37
+ } | null>;
38
+ export(ctx: HostContext, options: ExportOptions): Promise<ExportSection<FilesSection>>;
39
+ /** 单元清单(零 I/O:直接由 export 产物归并)。 */
40
+ listUnits(section: ExportSection<FilesSection>): ExportUnit[];
12
41
  analyzeImport(data: FilesSection, ctx: ImportContext): Promise<PlanItem[]>;
13
42
  applyItem(item: PlanItem, ctx: ImportContext): Promise<ApplyResult>;
14
43
  validate(data: FilesSection, msg?: MsgFunc): Promise<ValidationResult>;
@@ -12,10 +12,45 @@ import { sha256Hex } from '../utils/hashing.js';
12
12
  import { isReservedInternalRel, normalizePath } from '../utils/paths.js';
13
13
  import { msgOf, zhMsg } from '../core/messages.js';
14
14
  import { linkWarnings, listFilesDetailed } from './link-report.js';
15
+ import { defaultUnitId, unitAllowed, unitsFromFiles } from './units.js';
15
16
  export class FileCollectionAdapter {
16
- async export(ctx, _options) {
17
+ /**
18
+ * 最小可拆单元的 id(相对 baseDir 的路径)。
19
+ *
20
+ * 缺省 = **首个路径段** —— 目录 bundle 整体成一个单元,这是技能/会话/预设的正确粒度
21
+ * (拆开即失效)。平铺文件即自身。pluginFiles / self 覆写为「整条相对路径」:
22
+ * 它们的白名单文件彼此独立,不构成 bundle。
23
+ */
24
+ unitIdOf(relativePath) {
25
+ return defaultUnitId(relativePath);
26
+ }
27
+ /** 清单里的路径 → 相对 baseDir 的路径(baseDir 为空表示整目录即根)。 */
28
+ relPathOf(rel) {
29
+ if (this.baseDir === '')
30
+ return rel;
31
+ const prefix = this.baseDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
32
+ return rel.replace(new RegExp('^' + prefix + '[\\\\/]'), '');
33
+ }
34
+ /**
35
+ * 条目级选择(Phase 1):只导出白名单命中的单元。
36
+ *
37
+ * 过滤发生在 readFile **之前** —— 未勾选的文件不读盘,大分区(sessions)取消勾选后
38
+ * 导出耗时应显著下降。allow === undefined(键缺省)= 全量,与改造前完全一致。
39
+ */
40
+ /**
41
+ * 单元级预筛选钩子(缺省 null = 不筛选)。
42
+ *
43
+ * 子类可据 `options` 与清单计算「允许导出的单元」子集(如 sessions 的「最新 N 个」,
44
+ * 见 core/session-select.ts);`keep: null` = 本次不施加单元级筛选(但仍可带告警)。
45
+ * 在 readFile **之前**执行:未入选的文件不读盘。
46
+ */
47
+ async restrictUnits(_ctx, _rels, _options) {
48
+ return null;
49
+ }
50
+ async export(ctx, options) {
17
51
  const files = [];
18
52
  const warnings = [];
53
+ const allow = options.includeItems?.[this.id];
19
54
  // issue #37:用「跟随 junction/符号链接」的遍历,并把跟随/跳过的链接写进告警——
20
55
  // 此前链接目录及其全部内容被静默排除,备份仍报成功。
21
56
  let listing = { paths: [], skippedLinks: [], followedLinks: 0, unreadableDirs: [] };
@@ -26,9 +61,19 @@ export class FileCollectionAdapter {
26
61
  // 目录不存在视为空
27
62
  }
28
63
  const rels = listing.paths;
64
+ // 单元级预筛选(issue #39 Feature 1:sessions 的「最新 N 个」)
65
+ const restricted = await this.restrictUnits(ctx, rels, options);
66
+ if (restricted !== null)
67
+ warnings.push(...restricted.warnings);
68
+ const keep = restricted?.keep ?? null;
29
69
  for (const rel of rels) {
70
+ const relPath = this.relPathOf(rel);
71
+ const unitId = this.unitIdOf(relPath);
72
+ if (keep !== null && !keep.has(unitId))
73
+ continue;
74
+ if (!unitAllowed(allow, `${this.id}:${unitId}`))
75
+ continue;
30
76
  const data = await ctx.fs.readFile(rel);
31
- const relPath = this.baseDir === '' ? rel : rel.replace(new RegExp(`^${escapeRegExp(this.baseDir)}[\\/]`), '');
32
77
  files.push({ relativePath: relPath, data, contentHash: sha256Hex(data) });
33
78
  }
34
79
  if (rels.length === 0)
@@ -41,17 +86,24 @@ export class FileCollectionAdapter {
41
86
  warnings,
42
87
  };
43
88
  }
89
+ /** 单元清单(零 I/O:直接由 export 产物归并)。 */
90
+ listUnits(section) {
91
+ return unitsFromFiles(this.id, section.data.files, (rel) => this.unitIdOf(rel));
92
+ }
44
93
  async analyzeImport(data, ctx) {
45
94
  const msg = ctx.msg;
46
95
  const items = [];
47
96
  for (const file of data.files) {
48
97
  const id = `${this.id}:${file.relativePath}`;
98
+ // Phase 2:声明最小可拆单元(与 listUnits 同一套 unitIdOf 规则)。
99
+ // 计划项是逐文件的,但选择器要按「一个技能 bundle / 一次会话」勾选,两端因此对齐。
100
+ const unitId = `${this.id}:${this.unitIdOf(file.relativePath)}`;
49
101
  // F23 修复:不可信 import 不得写内部 control-plane namespace。
50
102
  // 检查 baseDir+ref 解析后的 homeDir 相对路径(self 适配器 baseDir='dsh-config-manager' 是主投毒向量)。
51
103
  const resolvedRel = normalizePath(path.join(this.baseDir, file.relativePath));
52
104
  if (isReservedInternalRel(resolvedRel)) {
53
105
  items.push({
54
- id, kind: 'Error', adapter: this.id,
106
+ id, unitId, kind: 'Error', adapter: this.id,
55
107
  description: msg('adapter.fileReserved', { path: file.relativePath }), severity: 'error',
56
108
  });
57
109
  continue;
@@ -65,17 +117,17 @@ export class FileCollectionAdapter {
65
117
  }
66
118
  if (current === null) {
67
119
  items.push({
68
- id, kind: 'Create', adapter: this.id,
120
+ id, unitId, kind: 'Create', adapter: this.id,
69
121
  description: msg('adapter.fileCreate', { type: this.displayName, path: file.relativePath }), severity: 'info',
70
122
  target: { adapter: this.id, ref: file.relativePath },
71
123
  });
72
124
  }
73
125
  else if (sha256Hex(current) === file.contentHash) {
74
- items.push({ id, kind: 'Skip', adapter: this.id, description: msg('adapter.fileSame', { path: file.relativePath }), severity: 'info' });
126
+ items.push({ id, unitId, kind: 'Skip', adapter: this.id, description: msg('adapter.fileSame', { path: file.relativePath }), severity: 'info' });
75
127
  }
76
128
  else {
77
129
  items.push({
78
- id, kind: 'Conflict', adapter: this.id,
130
+ id, unitId, kind: 'Conflict', adapter: this.id,
79
131
  description: msg('adapter.fileDiff', { path: file.relativePath }), severity: 'warning',
80
132
  target: { adapter: this.id, ref: file.relativePath },
81
133
  });
@@ -121,7 +173,4 @@ export class FileCollectionAdapter {
121
173
  return { valid: issues.filter((i) => i.severity === 'error').length === 0, issues };
122
174
  }
123
175
  }
124
- function escapeRegExp(s) {
125
- return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
126
- }
127
176
  //# sourceMappingURL=file-collection.js.map
@@ -1,6 +1,6 @@
1
1
  import type { MsgFunc } from '../core/messages.ts';
2
2
  import type { FilesSection } from '../schema/types.ts';
3
- import type { ApplyResult, ConfigAdapter, ExportOptions, ExportSection, HostContext, ImportContext, PlanItem, ValidationResult } from '../core/types.ts';
3
+ import type { ApplyResult, ConfigAdapter, ExportOptions, ExportSection, ExportUnit, HostContext, ImportContext, PlanItem, ValidationResult } from '../core/types.ts';
4
4
  export declare const DEFAULT_PLUGIN_FILE_WHITELIST: readonly string[];
5
5
  export declare class PluginFilesAdapter implements ConfigAdapter<FilesSection> {
6
6
  readonly id: 'pluginFiles';
@@ -11,7 +11,9 @@ export declare class PluginFilesAdapter implements ConfigAdapter<FilesSection> {
11
11
  /** 约定配置目录(相对 ~/.dsh 根,如 'plugin-config');递归收集其下所有文件。undefined = 不收集。 */
12
12
  private readonly collectDir?;
13
13
  constructor(whitelist?: string[], collectDir?: string);
14
- export(ctx: HostContext, _options: ExportOptions): Promise<ExportSection<FilesSection>>;
14
+ export(ctx: HostContext, options: ExportOptions): Promise<ExportSection<FilesSection>>;
15
+ /** 单元清单(零 I/O):逐文件单元,id 与导入侧 `pluginFiles:<relPath>` 一致。 */
16
+ listUnits(section: ExportSection<FilesSection>): ExportUnit[];
15
17
  analyzeImport(data: FilesSection, ctx: ImportContext): Promise<PlanItem[]>;
16
18
  applyItem(item: PlanItem, ctx: ImportContext): Promise<ApplyResult>;
17
19
  validate(data: FilesSection, msg?: MsgFunc): Promise<ValidationResult>;
@@ -10,6 +10,7 @@ import { sha256Hex } from '../utils/hashing.js';
10
10
  import { msgOf, zhMsg } from '../core/messages.js';
11
11
  import { linkWarnings, listFilesDetailed } from './link-report.js';
12
12
  import { isPathSafe, isReservedInternalRel } from '../utils/paths.js';
13
+ import { unitAllowed, unitsFromFiles } from './units.js';
13
14
  export const DEFAULT_PLUGIN_FILE_WHITELIST = ['dsh-ssh.json', 'pet.json'];
14
15
  export class PluginFilesAdapter {
15
16
  id = 'pluginFiles';
@@ -26,11 +27,16 @@ export class PluginFilesAdapter {
26
27
  }
27
28
  this.collectDir = collectDir === '' ? undefined : collectDir;
28
29
  }
29
- async export(ctx, _options) {
30
+ async export(ctx, options) {
30
31
  const files = [];
31
32
  const seen = new Set();
33
+ // Phase 1 条目级选择:**逐文件单元** —— 每个插件配置文件彼此独立,
34
+ // 不像技能/会话那样构成 bundle(collectDir 下每个文件可单独带走)。
35
+ const allow = options.includeItems?.[this.id];
32
36
  // 1) 白名单固定文件(不存在则跳过,dsh-ssh.json 等为按需创建)
33
37
  for (const rel of this.whitelist) {
38
+ if (!unitAllowed(allow, `${this.id}:${rel}`))
39
+ continue;
34
40
  try {
35
41
  const data = await ctx.fs.readFile(rel);
36
42
  files.push({ relativePath: rel, data, contentHash: sha256Hex(data) });
@@ -54,6 +60,8 @@ export class PluginFilesAdapter {
54
60
  for (const rel of rels) {
55
61
  if (seen.has(rel))
56
62
  continue;
63
+ if (!unitAllowed(allow, `${this.id}:${rel}`))
64
+ continue;
57
65
  try {
58
66
  const data = await ctx.fs.readFile(rel);
59
67
  files.push({ relativePath: rel, data, contentHash: sha256Hex(data) });
@@ -70,6 +78,10 @@ export class PluginFilesAdapter {
70
78
  warnings: linkWarnings(msgOf(ctx), this.displayName, listing),
71
79
  };
72
80
  }
81
+ /** 单元清单(零 I/O):逐文件单元,id 与导入侧 `pluginFiles:<relPath>` 一致。 */
82
+ listUnits(section) {
83
+ return unitsFromFiles(this.id, section.data.files, (rel) => rel);
84
+ }
73
85
  async analyzeImport(data, ctx) {
74
86
  const msg = ctx.msg;
75
87
  const items = [];
@@ -1,7 +1,7 @@
1
1
  import type { PackLocalPluginsResult } from '../core/local-plugin-pack.ts';
2
2
  import type { MsgFunc } from '../core/messages.ts';
3
3
  import type { PluginEntry, PluginsSection } from '../schema/types.ts';
4
- import type { ApplyResult, ConfigAdapter, ExportOptions, ExportSection, HostContext, ImportContext, PlanItem, ValidationResult } from '../core/types.ts';
4
+ import type { ApplyResult, ConfigAdapter, ExportOptions, ExportSection, ExportUnit, HostContext, ImportContext, PlanItem, ValidationResult } from '../core/types.ts';
5
5
  export declare const USER_PATCH_FILE = "cordis.patch.yml";
6
6
  /**
7
7
  * 本地源插件打包钩子(由宿主注入,见 src/index.ts createAdapters)。
@@ -28,7 +28,16 @@ export declare class PluginsAdapter implements ConfigAdapter<PluginsSection> {
28
28
  /** T1:本地源(link:/file:)插件打包钩子;未注入 = 不打包(保持改造前行为) */
29
29
  private readonly localPack;
30
30
  constructor(selfName?: string, localPack?: LocalPluginPackHook);
31
- export(ctx: HostContext, _options: ExportOptions): Promise<ExportSection<PluginsSection>>;
31
+ export(ctx: HostContext, options: ExportOptions): Promise<ExportSection<PluginsSection>>;
32
+ /**
33
+ * 单元清单(Phase 1;零 I/O:直接由 export 产物派生)。
34
+ *
35
+ * 单元 = 插件包 / cordis 补丁行 / pnpm-workspace.yaml / 单个 patch 文件。
36
+ * 后两者构成**原子组**:patchedDependencies 的声明与 patches/** 文件必须同进同出
37
+ * (issue #35 —— 只搬声明会让目标机 pnpm 拒绝一切 add),故互相写进 lockedWith,
38
+ * 由选择模型保证勾选/取消同步。
39
+ */
40
+ listUnits(section: ExportSection<PluginsSection>): ExportUnit[];
32
41
  /**
33
42
  * 导入后目标机**可能**存在的 patch 文件集合(相对 profile 目录)——issue #35。
34
43
  * = 备份携带的 patchFiles ∪ 目标机本来就有的文件。只用于判定「声明能否被满足」,
@@ -119,7 +119,7 @@ export class PluginsAdapter {
119
119
  this.selfName = selfName;
120
120
  this.localPack = localPack;
121
121
  }
122
- async export(ctx, _options) {
122
+ async export(ctx, options) {
123
123
  const plugins = [];
124
124
  const warnings = [];
125
125
  try {
@@ -228,25 +228,97 @@ export class PluginsAdapter {
228
228
  warnings.push(msgOf(ctx)('adapter.localPackFailed', { reason: err instanceof Error ? err.message : String(err) }));
229
229
  }
230
230
  }
231
+ // Phase 1 条目级选择:只保留白名单命中的单元。键缺省 = 全量(与改造前逐字节一致);
232
+ // 空数组由 Exporter 在选定阶段整分区剔除,不会走到这里。
233
+ const allow = options.includeItems?.[this.id];
234
+ let keptPlugins = effectivePlugins;
235
+ let keptPatch = patch;
236
+ let keptWorkspace = pnpmWorkspace;
237
+ let keptPatchFiles = patchFiles;
238
+ let keptTarballs = localTarballs;
239
+ if (allow !== undefined) {
240
+ // 原子组(D5 / issue #35):pnpm-workspace.yaml ↔ patch 文件必须同进同出 ——
241
+ // 声明在而文件不在会让目标机 pnpm 拒绝一切 add;文件在而声明不在则补丁静默失效。
242
+ // 这里用 every(全或无)做纵深防御:即使 UI 的 lockedWith 没生效也不会产出半套。
243
+ const wsId = 'plugins:pnpm-workspace';
244
+ const pfIds = (patchFiles ?? []).map((pf) => `plugins:patch:${normalizePath(pf.relativePath)}`);
245
+ const groupKept = [wsId, ...pfIds].every((id) => allow.includes(id));
246
+ keptWorkspace = groupKept ? pnpmWorkspace : null;
247
+ keptPatchFiles = groupKept ? patchFiles : undefined;
248
+ keptPlugins = effectivePlugins.filter((pl) => allow.includes(`plugin:${pl.name}`));
249
+ keptPatch = patch.filter((pl) => allow.includes(`patch:${pl.lineId}`));
250
+ // tarball 只跟随存活的插件(本地源插件的打包产物不得被孤立带走)
251
+ const alive = new Set(keptPlugins.map((pl) => pl.name));
252
+ keptTarballs = localTarballs?.filter((t) => alive.has(t.packageName));
253
+ }
231
254
  return {
232
255
  sectionId: 'plugins',
233
256
  data: {
234
257
  version: 1,
235
- plugins: effectivePlugins,
236
- patch,
237
- pnpmWorkspace,
238
- ...(localTarballs !== undefined ? { localTarballs } : {}),
239
- ...(patchFiles !== undefined ? { patchFiles } : {}),
258
+ plugins: keptPlugins,
259
+ patch: keptPatch,
260
+ pnpmWorkspace: keptWorkspace,
261
+ ...(keptTarballs !== undefined ? { localTarballs: keptTarballs } : {}),
262
+ ...(keptPatchFiles !== undefined ? { patchFiles: keptPatchFiles } : {}),
240
263
  },
241
264
  counts: {
242
- plugins: effectivePlugins.length,
243
- patchLines: patch.length,
244
- ...(localTarballs !== undefined ? { localTarballs: localTarballs.length } : {}),
245
- ...(patchFiles !== undefined ? { patchFiles: patchFiles.length } : {}),
265
+ plugins: keptPlugins.length,
266
+ patchLines: keptPatch.length,
267
+ ...(keptTarballs !== undefined ? { localTarballs: keptTarballs.length } : {}),
268
+ ...(keptPatchFiles !== undefined ? { patchFiles: keptPatchFiles.length } : {}),
246
269
  },
247
270
  warnings,
248
271
  };
249
272
  }
273
+ /**
274
+ * 单元清单(Phase 1;零 I/O:直接由 export 产物派生)。
275
+ *
276
+ * 单元 = 插件包 / cordis 补丁行 / pnpm-workspace.yaml / 单个 patch 文件。
277
+ * 后两者构成**原子组**:patchedDependencies 的声明与 patches/** 文件必须同进同出
278
+ * (issue #35 —— 只搬声明会让目标机 pnpm 拒绝一切 add),故互相写进 lockedWith,
279
+ * 由选择模型保证勾选/取消同步。
280
+ */
281
+ listUnits(section) {
282
+ const data = section.data;
283
+ const wsId = 'plugins:pnpm-workspace';
284
+ const patchFileIds = (data.patchFiles ?? []).map((pf) => `plugins:patch:${normalizePath(pf.relativePath)}`);
285
+ const group = [wsId, ...patchFileIds];
286
+ const units = [];
287
+ for (const pl of data.plugins) {
288
+ const tarball = (data.localTarballs ?? []).find((t) => t.packageName === pl.name);
289
+ units.push({
290
+ id: `plugin:${pl.name}`,
291
+ label: pl.name,
292
+ detail: pl.version,
293
+ // base64 长度 → 原始字节数(仅用于展示体积,不求精确)
294
+ sizeBytes: tarball !== undefined ? Math.floor((tarball.base64.length * 3) / 4) : 0,
295
+ ...(tarball !== undefined ? { fileCount: 1 } : {}),
296
+ });
297
+ }
298
+ for (const line of data.patch) {
299
+ // 由 mcp / prompts adapter 管理的行不属本分区(applyItem 同样跳过),列出来只会误导用户
300
+ if (isManagedElsewhere(line.raw))
301
+ continue;
302
+ units.push({ id: `patch:${line.lineId}`, label: line.lineId, sizeBytes: 0 });
303
+ }
304
+ if (data.pnpmWorkspace !== undefined && data.pnpmWorkspace !== null && data.pnpmWorkspace !== '') {
305
+ units.push({
306
+ id: wsId,
307
+ label: 'pnpm-workspace.yaml',
308
+ sizeBytes: data.pnpmWorkspace.length,
309
+ ...(group.length > 1 ? { lockedWith: group } : {}),
310
+ });
311
+ }
312
+ for (const pf of data.patchFiles ?? []) {
313
+ units.push({
314
+ id: `plugins:patch:${normalizePath(pf.relativePath)}`,
315
+ label: pf.relativePath,
316
+ sizeBytes: Math.floor((pf.base64.length * 3) / 4),
317
+ ...(group.length > 1 ? { lockedWith: group } : {}),
318
+ });
319
+ }
320
+ return units;
321
+ }
250
322
  /**
251
323
  * 导入后目标机**可能**存在的 patch 文件集合(相对 profile 目录)——issue #35。
252
324
  * = 备份携带的 patchFiles ∪ 目标机本来就有的文件。只用于判定「声明能否被满足」,
@@ -13,5 +13,8 @@ export declare class SelfAdapter extends FileCollectionAdapter {
13
13
  constructor(baseDir?: string);
14
14
  /** 白名单收集:只导出配置类文件(存在才收),不递归(排除快照/历史/缓存/临时产物)。
15
15
  * relativePath 产出相对 baseDir 的路径(与 analyzeImport/applyItem 的 path.join(baseDir, ref) 匹配)。 */
16
- export(ctx: HostContext, _options: ExportOptions): Promise<ExportSection<FilesSection>>;
16
+ /** self 的白名单文件彼此独立(同步配置 / 市场配置 / UI 偏好…),不构成 bundle
17
+ * → 覆写为「逐文件单元」,用户可以只带走其中几项。 */
18
+ protected unitIdOf(relativePath: string): string;
19
+ export(ctx: HostContext, options: ExportOptions): Promise<ExportSection<FilesSection>>;
17
20
  }
@@ -25,6 +25,7 @@ import path from 'node:path';
25
25
  import { msgOf } from '../core/messages.js';
26
26
  import { sha256Hex } from '../utils/hashing.js';
27
27
  import { FileCollectionAdapter } from './file-collection.js';
28
+ import { unitAllowed } from './units.js';
28
29
  /** self 分区白名单文件(相对 baseDir,即 $DSH_HOME/dsh-config-manager/)。 */
29
30
  export const SELF_CONFIG_FILES = [
30
31
  'sync/sync-config.json',
@@ -50,10 +51,18 @@ export class SelfAdapter extends FileCollectionAdapter {
50
51
  }
51
52
  /** 白名单收集:只导出配置类文件(存在才收),不递归(排除快照/历史/缓存/临时产物)。
52
53
  * relativePath 产出相对 baseDir 的路径(与 analyzeImport/applyItem 的 path.join(baseDir, ref) 匹配)。 */
53
- async export(ctx, _options) {
54
+ /** self 的白名单文件彼此独立(同步配置 / 市场配置 / UI 偏好…),不构成 bundle
55
+ * → 覆写为「逐文件单元」,用户可以只带走其中几项。 */
56
+ unitIdOf(relativePath) {
57
+ return relativePath;
58
+ }
59
+ async export(ctx, options) {
54
60
  const files = [];
55
61
  const warnings = [];
62
+ const allow = options.includeItems?.[this.id];
56
63
  for (const rel of SELF_CONFIG_FILES) {
64
+ if (!unitAllowed(allow, `${this.id}:${this.unitIdOf(rel)}`))
65
+ continue;
57
66
  // ctx.fs.readFile 语义 = 相对 homeDir 的完整路径 → 拼接 baseDir;产出仍为相对 baseDir
58
67
  const data = await ctx.fs.readFile(path.join(this.baseDir, rel)).catch(() => null);
59
68
  if (data === null)