dsh-config-manager 0.1.78 → 0.1.80

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 (302) hide show
  1. package/lib/adapters/credentials.d.ts +6 -2
  2. package/lib/adapters/credentials.js +19 -3
  3. package/lib/adapters/file-collection.js +2 -1
  4. package/lib/adapters/mcp.d.ts +48 -2
  5. package/lib/adapters/mcp.js +265 -3
  6. package/lib/adapters/messages.d.ts +3 -0
  7. package/lib/adapters/messages.js +7 -0
  8. package/lib/adapters/plugin-files.js +2 -1
  9. package/lib/adapters/plugins.js +105 -20
  10. package/lib/adapters/pnpm-workspace.d.ts +32 -0
  11. package/lib/adapters/pnpm-workspace.js +113 -2
  12. package/lib/adapters/prompts.d.ts +6 -2
  13. package/lib/adapters/prompts.js +17 -2
  14. package/lib/adapters/providers.d.ts +19 -2
  15. package/lib/adapters/providers.js +59 -3
  16. package/lib/adapters/settings.d.ts +6 -2
  17. package/lib/adapters/settings.js +13 -2
  18. package/lib/adapters/skills.d.ts +25 -1
  19. package/lib/adapters/skills.js +60 -0
  20. package/lib/adapters/ui.d.ts +6 -2
  21. package/lib/adapters/ui.js +13 -2
  22. package/lib/adapters/units.d.ts +18 -0
  23. package/lib/adapters/units.js +39 -0
  24. package/lib/adapters/workspaces.js +2 -1
  25. package/lib/client.d.ts +401 -112
  26. package/lib/client.js +2899 -1525
  27. package/lib/core/backup.js +20 -0
  28. package/lib/core/mcp-connector-store.d.ts +159 -0
  29. package/lib/core/mcp-connector-store.js +445 -0
  30. package/lib/core/messages-core.d.ts +2 -0
  31. package/lib/core/messages-core.js +4 -0
  32. package/lib/core/messages.d.ts +20 -0
  33. package/lib/core/messages.js +44 -0
  34. package/lib/core/phase3-host.js +18 -1
  35. package/lib/core/plugin-cli.d.ts +22 -2
  36. package/lib/core/plugin-cli.js +22 -3
  37. package/lib/core/recovery-orchestrator.d.ts +3 -1
  38. package/lib/core/recovery-orchestrator.js +75 -16
  39. package/lib/core/rollback-activity.d.ts +89 -0
  40. package/lib/core/rollback-activity.js +350 -0
  41. package/lib/core/rollback-ledger.d.ts +0 -9
  42. package/lib/core/rollback-ledger.js +37 -0
  43. package/lib/core/rollback-merge.d.ts +22 -2
  44. package/lib/core/rollback-merge.js +63 -13
  45. package/lib/core/rollback-point.d.ts +93 -0
  46. package/lib/core/rollback-point.js +420 -101
  47. package/lib/core/rollback-undo-journal.d.ts +123 -0
  48. package/lib/core/rollback-undo-journal.js +517 -0
  49. package/lib/core/self-update.js +2 -4
  50. package/lib/core/text-fidelity.d.ts +89 -0
  51. package/lib/core/text-fidelity.js +176 -0
  52. package/lib/core/types.d.ts +12 -3
  53. package/lib/core/update-check.js +2 -4
  54. package/lib/core/validator.d.ts +9 -0
  55. package/lib/core/validator.js +12 -0
  56. package/lib/foreign/bundle.js +42 -3
  57. package/lib/foreign/claude-code.js +358 -26
  58. package/lib/foreign/claude-sessions.d.ts +51 -8
  59. package/lib/foreign/claude-sessions.js +111 -13
  60. package/lib/foreign/read-claude-code.d.ts +33 -4
  61. package/lib/foreign/read-claude-code.js +446 -65
  62. package/lib/foreign/read-claude-plugins.d.ts +36 -0
  63. package/lib/foreign/read-claude-plugins.js +403 -0
  64. package/lib/foreign/registry.js +25 -2
  65. package/lib/foreign/truth-table.js +6 -4
  66. package/lib/foreign/types.d.ts +170 -2
  67. package/lib/index.d.ts +14 -5
  68. package/lib/index.js +60 -16
  69. package/lib/market/git-file-writer.js +7 -1
  70. package/lib/profiles/dsh-profile-launcher.d.ts +20 -0
  71. package/lib/profiles/dsh-profile-launcher.js +58 -1
  72. package/lib/profiles/dsh-profile-shared.d.ts +8 -2
  73. package/lib/profiles/dsh-profile-shared.js +8 -2
  74. package/lib/routes/foreign.js +8 -1
  75. package/lib/routes/index.d.ts +1 -1
  76. package/lib/routes/kit.d.ts +2 -2
  77. package/lib/routes/sync.js +173 -9
  78. package/lib/schema/manifest.d.ts +6 -1
  79. package/lib/schema/manifest.js +55 -0
  80. package/lib/schema/types.d.ts +47 -2
  81. package/lib/sync/ancestor.d.ts +22 -0
  82. package/lib/sync/ancestor.js +66 -2
  83. package/lib/sync/autosync-config.d.ts +73 -0
  84. package/lib/sync/autosync-config.js +89 -0
  85. package/lib/sync/autosync-scheduler.d.ts +80 -6
  86. package/lib/sync/autosync-scheduler.js +263 -40
  87. package/lib/sync/gist/gist-transport.d.ts +18 -1
  88. package/lib/sync/gist/gist-transport.js +42 -3
  89. package/lib/sync/gist/messages.d.ts +2 -0
  90. package/lib/sync/gist/messages.js +5 -0
  91. package/lib/sync/git/git-transport.d.ts +42 -3
  92. package/lib/sync/git/git-transport.js +146 -3
  93. package/lib/sync/merge.d.ts +47 -14
  94. package/lib/sync/merge.js +242 -222
  95. package/lib/sync/messages.d.ts +8 -0
  96. package/lib/sync/messages.js +19 -0
  97. package/lib/sync/risk.d.ts +19 -2
  98. package/lib/sync/risk.js +10 -3
  99. package/lib/sync/s3/messages.d.ts +6 -0
  100. package/lib/sync/s3/messages.js +13 -0
  101. package/lib/sync/s3/s3-transport.d.ts +9 -1
  102. package/lib/sync/s3/s3-transport.js +78 -6
  103. package/lib/sync/s3/sigv4.js +11 -2
  104. package/lib/sync/sync-engine.d.ts +212 -8
  105. package/lib/sync/sync-engine.js +514 -58
  106. package/lib/sync/sync-history.d.ts +11 -0
  107. package/lib/sync/sync-history.js +20 -0
  108. package/lib/sync/sync-state.d.ts +43 -0
  109. package/lib/sync/sync-state.js +44 -0
  110. package/lib/sync/sync-watcher.d.ts +76 -0
  111. package/lib/sync/sync-watcher.js +202 -0
  112. package/lib/sync/text-merge.d.ts +85 -0
  113. package/lib/sync/text-merge.js +513 -0
  114. package/lib/sync/transport.d.ts +48 -0
  115. package/lib/sync/transport.js +12 -0
  116. package/lib/sync/watch-scope.d.ts +38 -0
  117. package/lib/sync/watch-scope.js +95 -0
  118. package/lib/sync/webdav/webdav-transport.d.ts +16 -2
  119. package/lib/sync/webdav/webdav-transport.js +128 -11
  120. package/lib/ui/foreign-view.js +10 -0
  121. package/lib/ui/i18n.d.ts +9 -0
  122. package/lib/ui/i18n.js +22 -0
  123. package/lib/ui/rollback-view.d.ts +12 -0
  124. package/lib/ui/rollback-view.js +2 -0
  125. package/lib/ui/sync-settings-view.d.ts +47 -0
  126. package/lib/ui/sync-settings-view.js +24 -0
  127. package/lib/ui/types.d.ts +21 -0
  128. package/lib/utils/env-lock.d.ts +1 -0
  129. package/lib/utils/env-lock.js +7 -4
  130. package/package.json +6 -6
  131. package/src/adapters/credentials.ts +22 -4
  132. package/src/adapters/file-collection.ts +2 -1
  133. package/src/adapters/mcp.test.ts +235 -1
  134. package/src/adapters/mcp.ts +276 -5
  135. package/src/adapters/messages.ts +7 -0
  136. package/src/adapters/plugin-files.ts +2 -1
  137. package/src/adapters/plugins.test.ts +176 -2
  138. package/src/adapters/plugins.ts +107 -21
  139. package/src/adapters/pnpm-workspace.test.ts +89 -1
  140. package/src/adapters/pnpm-workspace.ts +127 -2
  141. package/src/adapters/prompts.ts +20 -3
  142. package/src/adapters/providers.ts +62 -4
  143. package/src/adapters/settings.ts +16 -3
  144. package/src/adapters/skills.test.ts +141 -0
  145. package/src/adapters/skills.ts +58 -2
  146. package/src/adapters/ui.ts +16 -3
  147. package/src/adapters/units-entries.test.ts +190 -0
  148. package/src/adapters/units.ts +50 -0
  149. package/src/adapters/workspaces.ts +2 -1
  150. package/src/client/about/PluginUpdateDialog.tsx +4 -2
  151. package/src/client/about/ReleaseNotesDialog.tsx +4 -2
  152. package/src/client/chat/RollbackReviewDialog.tsx +3 -2
  153. package/src/client/common/Skeleton.tsx +32 -0
  154. package/src/client/common/api-base.test.ts +36 -0
  155. package/src/client/common/http.test.ts +12 -1
  156. package/src/client/common/http.ts +8 -2
  157. package/src/client/common/routes.ts +46 -3
  158. package/src/client/config-manager.module.css +321 -11
  159. package/src/client/home/HomePanel.tsx +14 -6
  160. package/src/client/import/ForeignImportView.tsx +3 -1
  161. package/src/client/import/import-wizard-steps.tsx +2 -1
  162. package/src/client/library/ArtifactRow.tsx +12 -2
  163. package/src/client/library/LibraryActions.tsx +6 -3
  164. package/src/client/library/LibraryPanel.tsx +21 -3
  165. package/src/client/library/RestorePlanView.tsx +4 -2
  166. package/src/client/locales.ts +3 -0
  167. package/src/client/market/market-api.test.ts +3 -2
  168. package/src/client/recovery/RecoveryPanel.tsx +54 -7
  169. package/src/client/recovery/RollbackCard.tsx +7 -2
  170. package/src/client/recovery/RollbackDiffPane.tsx +3 -2
  171. package/src/client/recovery/recovery-locales.ts +20 -2
  172. package/src/client/recovery/recovery-view.test.ts +39 -1
  173. package/src/client/recovery/recovery-view.ts +23 -1
  174. package/src/client/run-store.test.ts +1 -1
  175. package/src/client/run-store.ts +4 -0
  176. package/src/client/sync/AutosyncCard.tsx +49 -6
  177. package/src/client/sync/MergeModal.tsx +220 -0
  178. package/src/client/sync/MergePreviewView.tsx +306 -0
  179. package/src/client/sync/SyncActivityBar.tsx +31 -0
  180. package/src/client/sync/SyncConfirmView.tsx +2 -1
  181. package/src/client/sync/SyncPanel.tsx +130 -87
  182. package/src/client/sync/SyncProbeRow.tsx +162 -0
  183. package/src/client/sync/SyncSettingsView.test.ts +27 -2
  184. package/src/client/sync/WorkDirCard.tsx +44 -6
  185. package/src/client/sync/history-model.ts +4 -1
  186. package/src/client/sync/merge-view.test.ts +248 -0
  187. package/src/client/sync/merge-view.ts +398 -0
  188. package/src/client/sync/probe-view.test.ts +90 -0
  189. package/src/client/sync/probe-view.ts +63 -0
  190. package/src/client/sync/remote-device-view.test.ts +35 -0
  191. package/src/client/sync/remote-device-view.ts +25 -0
  192. package/src/client/sync/sync-api.test.ts +5 -3
  193. package/src/client/sync/sync-api.ts +212 -2
  194. package/src/client/sync/sync-entry.test.ts +78 -0
  195. package/src/client/sync/sync-entry.ts +53 -0
  196. package/src/client/sync/sync-locales.ts +234 -2
  197. package/src/client/sync/sync-view.test.ts +18 -1
  198. package/src/client/sync/sync-view.ts +20 -0
  199. package/src/core/backup.test.ts +41 -0
  200. package/src/core/backup.ts +20 -0
  201. package/src/core/mcp-connector-store.test.ts +265 -0
  202. package/src/core/mcp-connector-store.ts +516 -0
  203. package/src/core/messages-core.ts +4 -0
  204. package/src/core/messages.ts +46 -0
  205. package/src/core/phase3-host.ts +18 -1
  206. package/src/core/phase3-production-integration.test.ts +4 -0
  207. package/src/core/plugin-cli.test.ts +26 -0
  208. package/src/core/plugin-cli.ts +30 -4
  209. package/src/core/recovery-orchestrator.test.ts +104 -5
  210. package/src/core/recovery-orchestrator.ts +74 -17
  211. package/src/core/rollback-activity.test.ts +311 -0
  212. package/src/core/rollback-activity.ts +367 -0
  213. package/src/core/rollback-cross-session.test.ts +694 -0
  214. package/src/core/rollback-interrupted.test.ts +137 -0
  215. package/src/core/rollback-ledger.ts +32 -0
  216. package/src/core/rollback-merge.test.ts +130 -0
  217. package/src/core/rollback-merge.ts +78 -14
  218. package/src/core/rollback-point.ts +468 -91
  219. package/src/core/rollback-undo-journal.test.ts +508 -0
  220. package/src/core/rollback-undo-journal.ts +571 -0
  221. package/src/core/self-update.ts +2 -5
  222. package/src/core/text-fidelity.test.ts +189 -0
  223. package/src/core/text-fidelity.ts +197 -0
  224. package/src/core/types.ts +11 -3
  225. package/src/core/update-check.ts +2 -5
  226. package/src/core/validator.ts +14 -0
  227. package/src/foreign/bundle-origin.test.ts +265 -0
  228. package/src/foreign/bundle.ts +45 -5
  229. package/src/foreign/claude-code.test.ts +268 -0
  230. package/src/foreign/claude-code.ts +406 -25
  231. package/src/foreign/claude-plugins.test.ts +234 -0
  232. package/src/foreign/claude-sessions.test.ts +141 -0
  233. package/src/foreign/claude-sessions.ts +179 -23
  234. package/src/foreign/file-budget.test.ts +4 -1
  235. package/src/foreign/read-claude-code.test.ts +320 -0
  236. package/src/foreign/read-claude-code.ts +490 -71
  237. package/src/foreign/read-claude-plugins.ts +465 -0
  238. package/src/foreign/registry.ts +25 -2
  239. package/src/foreign/session-ir.test.ts +65 -0
  240. package/src/foreign/truth-table.test.ts +81 -0
  241. package/src/foreign/truth-table.ts +6 -3
  242. package/src/foreign/types.ts +162 -2
  243. package/src/index.facade.test.ts +26 -4
  244. package/src/index.ts +76 -19
  245. package/src/market/git-file-writer.test.ts +4 -0
  246. package/src/market/git-file-writer.ts +7 -1
  247. package/src/profiles/dsh-profile-launcher.test.ts +68 -3
  248. package/src/profiles/dsh-profile-launcher.ts +56 -1
  249. package/src/profiles/dsh-profile-shared.ts +8 -2
  250. package/src/routes/foreign.ts +8 -1
  251. package/src/routes/index.ts +1 -1
  252. package/src/routes/kit.ts +2 -2
  253. package/src/routes/sync.ts +176 -9
  254. package/src/schema/manifest-schema.test.ts +87 -2
  255. package/src/schema/manifest.ts +57 -1
  256. package/src/schema/types.ts +45 -2
  257. package/src/sync/ancestor.ts +76 -2
  258. package/src/sync/autosync-config.test.ts +73 -1
  259. package/src/sync/autosync-config.ts +124 -0
  260. package/src/sync/autosync-risk-tier.test.ts +38 -2
  261. package/src/sync/autosync-scheduler.test.ts +264 -12
  262. package/src/sync/autosync-scheduler.ts +296 -44
  263. package/src/sync/gist/gist-conditional-write.test.ts +203 -0
  264. package/src/sync/gist/gist-transport.ts +53 -5
  265. package/src/sync/gist/messages.ts +5 -0
  266. package/src/sync/git/git-probe.test.ts +80 -0
  267. package/src/sync/git/git-transport.test.ts +185 -0
  268. package/src/sync/git/git-transport.ts +178 -5
  269. package/src/sync/merge.test.ts +188 -55
  270. package/src/sync/merge.ts +282 -224
  271. package/src/sync/messages.ts +19 -0
  272. package/src/sync/risk.ts +29 -5
  273. package/src/sync/s3/messages.ts +13 -0
  274. package/src/sync/s3/s3-conditional-write.test.ts +330 -0
  275. package/src/sync/s3/s3-transport.test.ts +2 -1
  276. package/src/sync/s3/s3-transport.ts +87 -9
  277. package/src/sync/s3/sigv4.test.ts +4 -1
  278. package/src/sync/s3/sigv4.ts +11 -2
  279. package/src/sync/sync-device-identity.test.ts +267 -0
  280. package/src/sync/sync-engine.test.ts +410 -19
  281. package/src/sync/sync-engine.ts +602 -57
  282. package/src/sync/sync-history.test.ts +16 -1
  283. package/src/sync/sync-history.ts +26 -0
  284. package/src/sync/sync-state.test.ts +64 -0
  285. package/src/sync/sync-state.ts +79 -0
  286. package/src/sync/sync-watcher.test.ts +197 -0
  287. package/src/sync/sync-watcher.ts +246 -0
  288. package/src/sync/text-merge.test.ts +358 -0
  289. package/src/sync/text-merge.ts +554 -0
  290. package/src/sync/transport.ts +57 -0
  291. package/src/sync/watch-scope.test.ts +122 -0
  292. package/src/sync/watch-scope.ts +120 -0
  293. package/src/sync/webdav/webdav-conditional-write.test.ts +360 -0
  294. package/src/sync/webdav/webdav-transport.ts +137 -14
  295. package/src/ui/foreign-view.test.ts +34 -0
  296. package/src/ui/foreign-view.ts +10 -0
  297. package/src/ui/i18n.ts +22 -0
  298. package/src/ui/rollback-view.ts +14 -0
  299. package/src/ui/sync-settings-view.test.ts +28 -1
  300. package/src/ui/sync-settings-view.ts +43 -0
  301. package/src/ui/types.ts +22 -0
  302. package/src/utils/env-lock.ts +8 -5
@@ -0,0 +1,95 @@
1
+ /**
2
+ * m-autosync-realtime:**监听范围**(纯函数,host 侧)。
3
+ *
4
+ * 自动同步要「像 VS Code 一样」在本地改动后立刻上行,就必须知道**哪些磁盘路径承载了
5
+ * 可同步的分区内容**。这份映射不能从注册表的 ZIP 内路径推出来 —— 两者是两件事:
6
+ * - 注册表 `SECTION_JSON_PATHS` / `SECTION_FILE_PREFIXES` 说的是**快照 ZIP 内部**的布局;
7
+ * - 真正被监视的是 `$DSH_HOME` 下的落盘位置(`skills/`、`settings.yaml`、
8
+ * `profiles/<p>/cordis.patch.yml` … 见 `src/core/backup.ts` 的 FILE_BASES 与
9
+ * `src/core/patch-layers.ts` 的两层 patch)。
10
+ *
11
+ * 因此本文件是「分区 → home 相对路径」的**唯一静态表**,只做事实声明,不做 I/O。
12
+ * 监听器(`sync-watcher.ts`)只负责 fs.watch + 去抖;判定「变了哪些分区」始终回到引擎的
13
+ * `hasLocalChanges()`(按分区 hash 比对基线),这里绝不猜。
14
+ *
15
+ * 关键取舍(勿“优化”成监听整个 home):
16
+ * - **不递归监听 `$DSH_HOME` 根**:`sessions/`(会话日志,可能几个 GB)、
17
+ * `node_modules`、`dsh-config-manager/{sync/work,snapshots,exports,tmp}` 全在里面,
18
+ * 一旦监听就会自己把自己点着(同步写工作副本 → 触发 → 再同步),且与「sessions 绝不自动
19
+ * 上下行」的铁律直接冲突;
20
+ * - **只监听 `profiles/<p>` 下的具体文件与 `patches/`**,绝不递归 `profiles/`:
21
+ * 那下面有整个 pnpm 依赖树;
22
+ * - `self` 分区只监听白名单里那几个 `*.json`(与 `src/adapters/self.ts` 的
23
+ * SELF_CONFIG_FILES 对齐),**刻意不监听 `sync/sync-state.json` / `sync/sync-history.json` /
24
+ * `sync/sync-autosync.json` / `sync/backup-schedule.json`** —— 调度器每轮都会改写它们,
25
+ * 监听就是自激循环;
26
+ * - `node_modules` / `.git` / 原子写临时文件在任何层级都被过滤。
27
+ */
28
+ import path from 'node:path';
29
+ /** 任意层级都要过滤的目录名(依赖树、git 内部、同步工作副本)。 */
30
+ export const WATCH_IGNORED_SEGMENTS = ['node_modules', '.git'];
31
+ /** 任意层级都要过滤的文件名模式(原子写临时文件:`x.json.tmp-1234-0`)。 */
32
+ const TMP_FILE_RE = /\.tmp-/u;
33
+ /**
34
+ * 组装监听目标清单。
35
+ *
36
+ * 清单顺序即日志顺序(稳定,便于测试与排查);路径一律 posix 相对 `$DSH_HOME`。
37
+ */
38
+ export function buildWatchTargets(input) {
39
+ const profile = input.profile !== undefined && input.profile !== '' ? input.profile : 'web';
40
+ const profileDir = `profiles/${profile}`;
41
+ const dataBase = 'dsh-config-manager';
42
+ return [
43
+ // settings / providers / ui:同一个 settings.yaml 的顶层 namespace
44
+ { relPath: 'settings.yaml', kind: 'file', recursive: false, sections: ['settings', 'providers', 'ui'] },
45
+ // prompts / mcp:两层组合 patch 的 home 层
46
+ { relPath: 'cordis.patch.yml', kind: 'file', recursive: false, sections: ['prompts', 'mcp'] },
47
+ // agentInstructions:home 根 AGENTS.md
48
+ { relPath: 'AGENTS.md', kind: 'file', recursive: false, sections: ['agentInstructions'] },
49
+ // prompts / mcp / plugins:两层组合 patch 的 profile 层 + pnpm 工作区声明 + profile manifest
50
+ { relPath: `${profileDir}/cordis.patch.yml`, kind: 'file', recursive: false, sections: ['prompts', 'mcp', 'plugins'] },
51
+ { relPath: `${profileDir}/pnpm-workspace.yaml`, kind: 'file', recursive: false, sections: ['plugins'] },
52
+ { relPath: `${profileDir}/package.json`, kind: 'file', recursive: false, sections: ['plugins'] },
53
+ // plugins:pnpm patch 文件(`patchFile:<profile 相对路径>`)
54
+ { relPath: `${profileDir}/patches`, kind: 'dir', recursive: true, sections: ['plugins'] },
55
+ // skills / agentPresets:整棵目录
56
+ { relPath: 'skills', kind: 'dir', recursive: true, sections: ['skills'] },
57
+ { relPath: '.agent-presets', kind: 'dir', recursive: true, sections: ['agentPresets'] },
58
+ // self:**只**监听白名单配置文件(与 adapters/self.ts 的 SELF_CONFIG_FILES 对齐)。
59
+ // 刻意不含 sync-state.json / sync-history.json / sync-autosync.json / backup-schedule.json。
60
+ { relPath: `${dataBase}/sync/sync-config.json`, kind: 'file', recursive: false, sections: ['self'] },
61
+ { relPath: `${dataBase}/sync/sync-selection.json`, kind: 'file', recursive: false, sections: ['self'] },
62
+ { relPath: `${dataBase}/sync/ui-prefs.json`, kind: 'file', recursive: false, sections: ['self'] },
63
+ { relPath: `${dataBase}/market/market-config.json`, kind: 'file', recursive: false, sections: ['self'] },
64
+ { relPath: `${dataBase}/exports/.backup-notes.json`, kind: 'file', recursive: false, sections: ['self'] },
65
+ ];
66
+ }
67
+ /** 把任一绝对路径转成 home 相对 posix 路径;不在 home 内 → null。 */
68
+ export function toHomeRelPath(homeDir, absPath) {
69
+ const rel = path.relative(homeDir, absPath);
70
+ if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel))
71
+ return null;
72
+ return rel.split(path.sep).join('/');
73
+ }
74
+ /**
75
+ * 该 home 相对路径是否属于「绝不该触发同步」的噪声。
76
+ *
77
+ * 判据只有两类(宁少不多:漏过滤只是多跑一轮空同步,误过滤会漏掉真实改动):
78
+ * 依赖树 / git 内部目录,以及原子写临时文件。
79
+ */
80
+ export function isNoiseRelPath(relPath) {
81
+ if (relPath === '')
82
+ return true;
83
+ const segments = relPath.split('/');
84
+ for (const segment of segments) {
85
+ if (WATCH_IGNORED_SEGMENTS.includes(segment))
86
+ return true;
87
+ }
88
+ const base = segments[segments.length - 1] ?? '';
89
+ return TMP_FILE_RE.test(base);
90
+ }
91
+ /** 监视目标的绝对路径。 */
92
+ export function targetAbsPath(homeDir, target) {
93
+ return path.join(homeDir, target.relPath.split('/').join(path.sep));
94
+ }
95
+ //# sourceMappingURL=watch-scope.js.map
@@ -1,7 +1,7 @@
1
1
  import '../messages.ts';
2
2
  import type { MsgFunc } from '../../core/messages.ts';
3
3
  import { SyncTransportError } from '../transport.ts';
4
- import type { SyncRetryOptions, SyncSnapshot, SyncSnapshotMeta, SyncTransport, SyncTransportErrorOptions } from '../transport.ts';
4
+ import type { SyncProbeResult, SyncRetryOptions, SyncSnapshot, SyncSnapshotMeta, SyncTransport, SyncTransportErrorOptions } from '../transport.ts';
5
5
  /** 请求选项:headers / body / 覆盖默认超时(ms;0 = 不超时) */
6
6
  export interface WebDavRequestOptions {
7
7
  headers?: Record<string, string>;
@@ -119,8 +119,22 @@ export declare class WebDavTransport implements SyncTransport {
119
119
  private indexUrl;
120
120
  /** 读取一次 password(错误脱敏用) */
121
121
  private passwordOnce;
122
- /** 幂等创建 snapshots 集合:MKCOL;已存在(405/2xx/3xx)视为成功。 */
122
+ /** 幂等创建 snapshots 集合:MKCOL;已存在(2xx/3xx/405/409)视为成功。
123
+ *
124
+ * cloud-sync 借鉴项 ③(首连自建):目标目录的**上级**不存在时(新账号,或用户填了一个还没建的
125
+ * 目录),按 WebDAV 规范 MKCOL 缺父目录回 409 —— 此前用户必须先去网盘里手工建好目录,否则
126
+ * 首次同步必然失败。现在:直连 409 时逐级补齐 baseUrl 路径上的每一层,再重试一次。
127
+ * 返回**本次真正新建**的层级 URL(已存在的层级不进结果,探针据此如实回报,普通上传忽略返回值)。 */
123
128
  private ensureCollection;
129
+ /** 逐级 MKCOL 补齐 baseUrl 的每一层目录(只有缺父目录时才会走到这里)。 */
130
+ private ensureAncestors;
131
+ /** 条件写失败(HTTP 412):远端在本次读取之后被别人改过 —— 中止写入,让用户重跑(cloud-sync 借鉴项 ①)。 */
132
+ private concurrentWrite;
133
+ /**
134
+ * 连通性探针(cloud-sync 借鉴项 ②):补齐目标目录 → 证明这真的是一个可读的 WebDAV 集合
135
+ * → 读一次索引回报快照数。只读;唯一写副作用是幂等 MKCOL 补目录(那正是它要修的故障)。
136
+ */
137
+ probe(): Promise<SyncProbeResult>;
124
138
  /**
125
139
  * 读 index。**三态语义**(audit-sync sync-N1 / t66):
126
140
  * - 200 + 合法数组 → 返回条目(list 用);
@@ -60,6 +60,40 @@ const BLOBS_INDEX_FILE = 'blobs-index.json';
60
60
  /** 内容哈希形状(sha256 hex):blob 路径只接受它,杜绝路径穿越 */
61
61
  const BLOB_HASH_RE = /^[a-f0-9]{64}$/;
62
62
  const REDACTED = '[REDACTED]';
63
+ /**
64
+ * 幂等 MKCOL 的「已存在 / 已建好」状态码(cloud-sync 借鉴项 ③)。
65
+ * 405 = 资源已存在(WebDAV 规范下的常见回法);301/302/303 = 反向代理把 MKCOL 重定向到规范 URL;
66
+ * 409 = 目标已存在(坚果云等网盘对已存在目录回 409,见 cloud-sync 的兼容注释)。
67
+ */
68
+ const MKCOL_OK_STATUSES = new Set([200, 201, 204, 301, 302, 303, 405, 409]);
69
+ /**
70
+ * 直连目标集合时可判为「已建好 / 已存在」的状态码(**不含 409**)。
71
+ *
72
+ * 为什么把 409 排除:RFC 4918 §9.3.1 规定「父目录不存在」的正确回法就是 **409 Conflict**,
73
+ * 而部分网盘(坚果云等)对「目标已存在」也回 409 —— 首连直连时两种语义无法区分,
74
+ * 一律当「已存在」会让「上级目录没建」这条真实故障被吞掉(紧接着的 PUT 才会失败)。
75
+ * 因此 409 必须先走 ensureAncestors 补齐父目录再重试;只有**重试后**仍 409 才按「已存在」接受。
76
+ */
77
+ const MKCOL_EXISTS_STATUSES = new Set([200, 201, 204, 301, 302, 303, 405]);
78
+ /**
79
+ * 条件写头(cloud-sync 借鉴项 ①):拿到 ETag 就要求「远端仍是我读到的那一版」(If-Match);
80
+ * 索引**本来就不存在**(首次推送且远端确无内容)时用 If-None-Match: * 防两个设备同时首推;
81
+ * 其余情形(服务端不返回 ETag)返回空对象 ⇒ 退化为无条件写,绝不因为加了头把同步判成失败。
82
+ */
83
+ function conditionalHeaders(etag, indexMissing) {
84
+ if (etag !== undefined)
85
+ return { 'If-Match': etag };
86
+ return indexMissing ? { 'If-None-Match': '*' } : {};
87
+ }
88
+ /** URL 路径段编码:已百分号编码的段原样保留(先 decode 成功才 re-encode),失败则直接编码。 */
89
+ function encodeSegmentSafe(segment) {
90
+ try {
91
+ return encodeURIComponent(decodeURIComponent(segment));
92
+ }
93
+ catch {
94
+ return encodeURIComponent(segment);
95
+ }
96
+ }
63
97
  /** 自动跟随的重定向状态码(RFC 7231/9110) */
64
98
  const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
65
99
  /** 重定向最大跳数(RFC 7231 建议 ≤5;防 302 循环拖死请求) */
@@ -264,7 +298,7 @@ export class WebDavTransport {
264
298
  // → 必须照常上传,绝不跳过。
265
299
  const meta = computeSnapshotMeta(snapshot);
266
300
  // audit-sync sync-N1 覆盖面补全(t66,来自 t22 的 C7):upload 也是写路径,**不接受 404 = 空集合**。
267
- const { entries: idxBefore, missing: indexMissing } = await this.readIndexDetailed(pwd);
301
+ const { entries: idxBefore, missing: indexMissing, etag: idxEtag } = await this.readIndexDetailed(pwd);
268
302
  if (indexMissing && await this.remoteHasPriorContent(pwd)) {
269
303
  // 索引 404 只是**缺信息**(不是「远端没有快照」):远端已有历史内容时,按空索引做读改写会把
270
304
  // 权威 index.json PUT 成只有本条目 → 其余快照从列表整体消失,随后 delete + GC 会回收**仍被
@@ -296,7 +330,12 @@ export class WebDavTransport {
296
330
  const merged = idxBefore.filter((m) => m.id !== snapshot.id);
297
331
  merged.push(meta);
298
332
  merged.sort((a, b) => (a.createdAt < b.createdAt ? -1 : a.createdAt > b.createdAt ? 1 : 0));
299
- const putIdx = await this.send('PUT', idxUrl, pwd, { body: JSON.stringify(merged) });
333
+ const putIdx = await this.send('PUT', idxUrl, pwd, {
334
+ body: JSON.stringify(merged),
335
+ headers: conditionalHeaders(idxEtag, indexMissing),
336
+ });
337
+ if (putIdx.status === 412)
338
+ throw this.concurrentWrite(idxUrl, putIdx.status);
300
339
  if (!putIdx.ok) {
301
340
  throw new WebDavTransportError(await this.failText('PUT', idxUrl, putIdx, pwd), classifyHttpStatus(putIdx.status));
302
341
  }
@@ -338,7 +377,7 @@ export class WebDavTransport {
338
377
  // 是**缺信息**而非事实,沿用空索引继续会把 index.json 覆盖成 [](其余快照从列表整体消失)并让
339
378
  // gcBlobStore 以空引用集回收,删掉仍被现存快照文件引用的会话 blob(不可恢复)。
340
379
  // 与 F1 同口径:宁可显式失败,也不制造不可恢复的远端状态。list() 仍保留「404 = 空集合」语义。
341
- const { entries: idx, missing: indexMissing } = await this.readIndexDetailed(pwd);
380
+ const { entries: idx, missing: indexMissing, etag: idxEtag } = await this.readIndexDetailed(pwd);
342
381
  // delete 快照文件:404 = 不存在,视为成功
343
382
  const url = this.snapshotUrl(id);
344
383
  const res = await this.send('DELETE', url, pwd);
@@ -355,7 +394,12 @@ export class WebDavTransport {
355
394
  if (remaining.length === idx.length && res.status === 404) {
356
395
  return; // 文件与 index 都不存在 → 静默成功,无写回
357
396
  }
358
- const putIdx = await this.send('PUT', idxUrl, pwd, { body: JSON.stringify(remaining) });
397
+ const putIdx = await this.send('PUT', idxUrl, pwd, {
398
+ body: JSON.stringify(remaining),
399
+ headers: conditionalHeaders(idxEtag, false),
400
+ });
401
+ if (putIdx.status === 412)
402
+ throw this.concurrentWrite(idxUrl, putIdx.status);
359
403
  if (!putIdx.ok) {
360
404
  throw new WebDavTransportError(await this.failText('PUT', idxUrl, putIdx, pwd), classifyHttpStatus(putIdx.status));
361
405
  }
@@ -624,14 +668,78 @@ export class WebDavTransport {
624
668
  return '';
625
669
  }
626
670
  }
627
- /** 幂等创建 snapshots 集合:MKCOL;已存在(405/2xx/3xx)视为成功。 */
671
+ /** 幂等创建 snapshots 集合:MKCOL;已存在(2xx/3xx/405/409)视为成功。
672
+ *
673
+ * cloud-sync 借鉴项 ③(首连自建):目标目录的**上级**不存在时(新账号,或用户填了一个还没建的
674
+ * 目录),按 WebDAV 规范 MKCOL 缺父目录回 409 —— 此前用户必须先去网盘里手工建好目录,否则
675
+ * 首次同步必然失败。现在:直连 409 时逐级补齐 baseUrl 路径上的每一层,再重试一次。
676
+ * 返回**本次真正新建**的层级 URL(已存在的层级不进结果,探针据此如实回报,普通上传忽略返回值)。 */
628
677
  async ensureCollection(pwd) {
629
678
  const url = this.snapshotsColUrl();
630
- const res = await this.send('MKCOL', url, pwd);
631
- const okStatuses = new Set([200, 201, 204, 301, 302, 303, 405]);
632
- if (okStatuses.has(res.status))
633
- return;
634
- throw new WebDavTransportError(await this.failText('MKCOL', url, res, pwd), classifyHttpStatus(res.status));
679
+ const first = await this.send('MKCOL', url, pwd);
680
+ // 首连只认「确定已存在 / 已建好」的状态;**409 不算**(二义:缺父目录 or 已存在)
681
+ if (MKCOL_EXISTS_STATUSES.has(first.status))
682
+ return [];
683
+ if (first.status !== 409) {
684
+ // 401/403/5xx/… 不是「目录缺失」,绝不吞掉:如实报错
685
+ throw new WebDavTransportError(await this.failText('MKCOL', url, first, pwd), classifyHttpStatus(first.status));
686
+ }
687
+ // 409:先逐级补齐 baseUrl 每一层(幂等;已存在的层回 405/3xx/409 都按已存在继续),再重试目标集合。
688
+ const created = await this.ensureAncestors(pwd);
689
+ const retry = await this.send('MKCOL', url, pwd);
690
+ // 重试真建出目标集合(201/200/204)也要如实回报 —— 否则界面会**低报**本次实际创建的目录
691
+ // (用户可见文案「已在远端自动创建目录」,少报一条等于说错)。
692
+ if (retry.status === 200 || retry.status === 201 || retry.status === 204)
693
+ created.push(url);
694
+ // 上级已确保存在:重试仍是 409 ⇒ 按「已存在」接受(部分网盘行为);真正的权限/路径错误由随后的 PUT 显式报出。
695
+ if (MKCOL_OK_STATUSES.has(retry.status))
696
+ return created;
697
+ throw new WebDavTransportError(await this.failText('MKCOL', url, retry, pwd), classifyHttpStatus(retry.status));
698
+ }
699
+ /** 逐级 MKCOL 补齐 baseUrl 的每一层目录(只有缺父目录时才会走到这里)。 */
700
+ async ensureAncestors(pwd) {
701
+ const base = new URL(this.o.baseUrl);
702
+ const segments = base.pathname.split('/').filter((seg) => seg !== '');
703
+ const created = [];
704
+ for (let depth = 1; depth <= segments.length; depth += 1) {
705
+ const path = segments.slice(0, depth).map((seg) => encodeSegmentSafe(seg)).join('/');
706
+ const url = `${base.origin}/${path}/`;
707
+ const res = await this.send('MKCOL', url, pwd);
708
+ // 201/200/204 = 这次真的建出来了;405/409/3xx = 早就存在 → 不写进结果(不谎报「已创建」)
709
+ if (res.status === 200 || res.status === 201 || res.status === 204)
710
+ created.push(url);
711
+ else if (MKCOL_OK_STATUSES.has(res.status))
712
+ continue;
713
+ else {
714
+ throw new WebDavTransportError(this.o.msg('sync.webdav.collectionCreateFailed', { path: url, status: String(res.status) }), classifyHttpStatus(res.status));
715
+ }
716
+ }
717
+ return created;
718
+ }
719
+ /** 条件写失败(HTTP 412):远端在本次读取之后被别人改过 —— 中止写入,让用户重跑(cloud-sync 借鉴项 ①)。 */
720
+ concurrentWrite(url, status) {
721
+ return new WebDavTransportError(this.o.msg('sync.webdav.concurrentWrite', { status: String(status), url }), { kind: 'conflict', retryable: false, status });
722
+ }
723
+ /**
724
+ * 连通性探针(cloud-sync 借鉴项 ②):补齐目标目录 → 证明这真的是一个可读的 WebDAV 集合
725
+ * → 读一次索引回报快照数。只读;唯一写副作用是幂等 MKCOL 补目录(那正是它要修的故障)。
726
+ */
727
+ async probe() {
728
+ const pwd = await this.passwordOnce();
729
+ const created = await this.ensureCollection(pwd);
730
+ const url = this.snapshotsColUrl();
731
+ const res = await this.send('PROPFIND', url, pwd, { headers: { Depth: '0' } });
732
+ // 2xx(含 207 Multi-Status)= 真的是 WebDAV 集合;405/501 = 服务端不支持 PROPFIND → 退回「读索引」当证据。
733
+ if (!res.ok && res.status !== 405 && res.status !== 501) {
734
+ throw new WebDavTransportError(this.o.msg('sync.webdav.probeUnreachable', { status: String(res.status), url }), classifyHttpStatus(res.status));
735
+ }
736
+ const { entries, missing } = await this.readIndexDetailed(pwd);
737
+ const count = missing ? 0 : entries.length;
738
+ return {
739
+ message: this.o.msg('sync.webdav.probeOk', { count: String(count) }),
740
+ snapshotCount: count,
741
+ ...(created.length > 0 ? { createdCollections: created } : {}),
742
+ };
635
743
  }
636
744
  /**
637
745
  * 读 index。**三态语义**(audit-sync sync-N1 / t66):
@@ -656,7 +764,14 @@ export class WebDavTransport {
656
764
  if (!res.ok) {
657
765
  throw new WebDavTransportError(await this.failText('GET', url, res, pwd), classifyHttpStatus(res.status));
658
766
  }
659
- return { entries: this.parseIndex(await res.text(), url, pwd), missing: false };
767
+ // ETag 是写路径**条件写**的前置条件(cloud-sync 借鉴项 ①):服务端给了就记下来,没给就退化成无条件写
768
+ // (绝不因为「服务端不返回 ETag」把同步判成失败)。
769
+ const etag = res.headers?.['etag'];
770
+ return {
771
+ entries: this.parseIndex(await res.text(), url, pwd),
772
+ missing: false,
773
+ ...(typeof etag === 'string' && etag !== '' ? { etag } : {}),
774
+ };
660
775
  }
661
776
  parseIndex(raw, url, pwd) {
662
777
  let parsed;
@@ -696,6 +811,8 @@ export class WebDavTransport {
696
811
  Authorization: auth,
697
812
  'Content-Type': 'application/json',
698
813
  'User-Agent': 'DSH-Config-Manager/0.1.55 (WebDAV Client)',
814
+ // 条件写头(If-Match / If-None-Match)与探针的 Depth 头从这里注入(cloud-sync 借鉴项 ①②)
815
+ ...(opts.headers ?? {}),
699
816
  };
700
817
  if (opts.body !== undefined)
701
818
  headers['Content-Length'] = String(Buffer.byteLength(opts.body, 'utf8'));
@@ -41,6 +41,16 @@ export const FOREIGN_SKIP_KEY = {
41
41
  // 档 B(t6)新增两条:把「推导出的 cwd」与「没有自动根」也变成可见事实,绝不静默
42
42
  'session-cwd-derived': 'foreign.skip.session-cwd-derived',
43
43
  'source-needs-explicit-path': 'foreign.skip.source-needs-explicit-path',
44
+ // 2026-10-18(8 个新冻结码):Claude Code 项目级 / 插件 / 权限 / 设置键诊断 + 会话 id 不一致 +
45
+ // MCP 同名 + 项目扫描触顶 —— 仍用全量 Record 兜住:新增码而没配文案 = 编译失败。
46
+ 'unsupported-permissions': 'foreign.skip.unsupported-permissions',
47
+ 'unsupported-settings-keys': 'foreign.skip.unsupported-settings-keys',
48
+ 'unsupported-agents': 'foreign.skip.unsupported-agents',
49
+ 'unsupported-rules': 'foreign.skip.unsupported-rules',
50
+ 'unsupported-plugins': 'foreign.skip.unsupported-plugins',
51
+ 'session-id-mismatch': 'foreign.skip.session-id-mismatch',
52
+ 'mcp-server-id-conflict': 'foreign.skip.mcp-server-id-conflict',
53
+ 'project-scan-truncated': 'foreign.skip.project-scan-truncated',
44
54
  };
45
55
  /**
46
56
  * 机器码 → 字典键。**未知码(未来版本 / 第三方宿主)回退键名本身**,
package/lib/ui/i18n.d.ts CHANGED
@@ -86,6 +86,7 @@ export declare const uiZh: {
86
86
  readonly 'report.unit.notes': '条说明';
87
87
  readonly 'report.action.done': '完成';
88
88
  readonly 'error.notMounted': 'config-manager 服务未挂载(插件未加载):请确认 profile 中已安装 dsh-config-manager 并重启 DSH';
89
+ readonly 'error.apiPathMissed': '接口路径未命中(HTTP {status}):请求前缀为 {base},但该路径没有返回 JSON —— 若你经反向代理挂在子路径下,请检查代理是否转发了这个前缀(issue #108)';
89
90
  readonly 'error.httpInvalidJson': 'HTTP {status}: 无效的 JSON 响应';
90
91
  readonly 'error.fallback': '操作失败';
91
92
  readonly 'error.requestTimeout': '请求超时({seconds} 秒):未在时限内收到宿主响应,请重试';
@@ -508,6 +509,14 @@ export declare const uiZh: {
508
509
  readonly 'foreign.skip.legacy-rules-file': '旧式规则文件:DSH 无对等结构,只报告';
509
510
  readonly 'foreign.skip.instructions-merged': '多份指令源被合并为一个全局指令文件';
510
511
  readonly 'foreign.skip.instructions-override-selected': '命中更高优先级的指令文件(override),按它导入';
512
+ readonly 'foreign.skip.unsupported-permissions': '权限规则(allow/ask/deny)未迁移:DSH 没有权限规则文件的对等结构,这些规则不在本包内,导入后不会生效';
513
+ readonly 'foreign.skip.unsupported-settings-keys': 'settings.json 里其余未迁移的顶层键:只记录键名与条数,键值一律不读取,也不会写进本包';
514
+ readonly 'foreign.skip.unsupported-agents': '.claude/agents/*.md 子代理定义未迁移:DSH 的子代理是运行时 provider 注册制,没有文件级对等结构';
515
+ readonly 'foreign.skip.unsupported-rules': '.claude/rules/*.md(带 frontmatter 作用域)未迁移:并入全局指令会丢掉作用域,故只报告不合并';
516
+ readonly 'foreign.skip.unsupported-plugins': 'Claude Code 插件本体未迁移:插件的 hooks / 命令靠 Claude 运行时执行,DSH 无法加载,插件代码一律不复制;插件自带的 skills 与 .mcp.json 里的 MCP 定义另行迁移';
517
+ readonly 'foreign.skip.session-id-mismatch': '会话记录里的 sessionId 与文件名不一致:DSH 按目录名/文件名定位会话,本次以文件名为准,记录里的 id 未采用';
518
+ readonly 'foreign.skip.mcp-server-id-conflict': '同名 MCP server 在多处出现(全局 / 项目级 / 插件):先到先得,其余同名项只报告未迁移';
519
+ readonly 'foreign.skip.project-scan-truncated': '项目较多:项目级扫描只覆盖了上限内的项目,其余项目本次未读取,其配置不在本包内';
511
520
  readonly 'foreign.lossy.file-section-secrets.title': '文件类分区的明文不做剥离';
512
521
  readonly 'foreign.lossy.file-section-secrets.detail': '技能 / Agent 预设 / 全局指令 / 插件文件 / 历史会话 / 插件自身配置这六个文件类分区承载的是原始文件内容:命中的明文原样进包(导入侧只报告、不改写;本页的产包路径连扫描都不做)。「默认不含秘密」只对结构化分区成立,对这类分区不成立 —— 需要保密请先清理源文件。';
513
522
  readonly 'foreign.lossy.entry-backslash.title': '条目名中段反斜杠:跨平台落点可能不同';
package/lib/ui/i18n.js CHANGED
@@ -93,6 +93,7 @@ export const uiZh = {
93
93
  'report.action.done': '完成',
94
94
  // 错误
95
95
  'error.notMounted': 'config-manager 服务未挂载(插件未加载):请确认 profile 中已安装 dsh-config-manager 并重启 DSH',
96
+ 'error.apiPathMissed': '接口路径未命中(HTTP {status}):请求前缀为 {base},但该路径没有返回 JSON —— 若你经反向代理挂在子路径下,请检查代理是否转发了这个前缀(issue #108)',
96
97
  'error.httpInvalidJson': 'HTTP {status}: 无效的 JSON 响应',
97
98
  'error.fallback': '操作失败',
98
99
  'error.requestTimeout': '请求超时({seconds} 秒):未在时限内收到宿主响应,请重试',
@@ -533,6 +534,17 @@ export const uiZh = {
533
534
  'foreign.skip.legacy-rules-file': '旧式规则文件:DSH 无对等结构,只报告',
534
535
  'foreign.skip.instructions-merged': '多份指令源被合并为一个全局指令文件',
535
536
  'foreign.skip.instructions-override-selected': '命中更高优先级的指令文件(override),按它导入',
537
+ // ---------- 2026-10-18:Claude Code 项目级配置 / 插件 / 权限 / 设置键的如实诊断(8 个新码)----------
538
+ // 纪律:每条都要能被代码逐条证明 —— 只说「什么没搬、为什么、本项是否可补」,
539
+ // 绝不写「已支持 / 完全兼容」这类无法被代码证明的话。
540
+ 'foreign.skip.unsupported-permissions': '权限规则(allow/ask/deny)未迁移:DSH 没有权限规则文件的对等结构,这些规则不在本包内,导入后不会生效',
541
+ 'foreign.skip.unsupported-settings-keys': 'settings.json 里其余未迁移的顶层键:只记录键名与条数,键值一律不读取,也不会写进本包',
542
+ 'foreign.skip.unsupported-agents': '.claude/agents/*.md 子代理定义未迁移:DSH 的子代理是运行时 provider 注册制,没有文件级对等结构',
543
+ 'foreign.skip.unsupported-rules': '.claude/rules/*.md(带 frontmatter 作用域)未迁移:并入全局指令会丢掉作用域,故只报告不合并',
544
+ 'foreign.skip.unsupported-plugins': 'Claude Code 插件本体未迁移:插件的 hooks / 命令靠 Claude 运行时执行,DSH 无法加载,插件代码一律不复制;插件自带的 skills 与 .mcp.json 里的 MCP 定义另行迁移',
545
+ 'foreign.skip.session-id-mismatch': '会话记录里的 sessionId 与文件名不一致:DSH 按目录名/文件名定位会话,本次以文件名为准,记录里的 id 未采用',
546
+ 'foreign.skip.mcp-server-id-conflict': '同名 MCP server 在多处出现(全局 / 项目级 / 插件):先到先得,其余同名项只报告未迁移',
547
+ 'foreign.skip.project-scan-truncated': '项目较多:项目级扫描只覆盖了上限内的项目,其余项目本次未读取,其配置不在本包内',
536
548
  // 三类已知有损(F-5)的**逐条文案**:由 src/ui/foreign-view.ts 的 foreignLossyRows 渲染,
537
549
  // 卡片标题 / 说明 / 适用性徽章属客户端外壳 chrome,键在 src/client/locales.ts。
538
550
  'foreign.lossy.file-section-secrets.title': '文件类分区的明文不做剥离',
@@ -656,6 +668,7 @@ export const uiEn = {
656
668
  'report.unit.notes': 'note(s)',
657
669
  'report.action.done': 'Done',
658
670
  'error.notMounted': 'config-manager service is not mounted (plugin not loaded): make sure dsh-config-manager is installed in the profile and restart DSH',
671
+ 'error.apiPathMissed': 'API path not matched (HTTP {status}): the request prefix is {base} but that path did not return JSON — if you serve DSH under a reverse-proxy sub-path, check that this prefix is proxied (issue #108)',
659
672
  'error.httpInvalidJson': 'HTTP {status}: invalid JSON response',
660
673
  'error.fallback': 'Operation failed',
661
674
  'error.requestTimeout': 'Request timed out ({seconds}s): the host did not respond in time, please retry',
@@ -1081,6 +1094,15 @@ export const uiEn = {
1081
1094
  'foreign.skip.legacy-rules-file': 'Legacy rules file: DSH has no equivalent, reported only',
1082
1095
  'foreign.skip.instructions-merged': 'Several instruction sources were merged into one global instruction file',
1083
1096
  'foreign.skip.instructions-override-selected': 'A higher-priority instruction file (override) was found and is the one imported',
1097
+ // 2026-10-18: honest diagnostics for Claude Code project-level config / plugins / permissions / settings keys (8 new codes)
1098
+ 'foreign.skip.unsupported-permissions': 'Permission rules (allow/ask/deny) were not migrated: DSH has no equivalent permission-rule file, so they are not in this bundle and will not take effect after import',
1099
+ 'foreign.skip.unsupported-settings-keys': 'Other unmigrated top-level keys in settings.json: only key names and counts are recorded; values are never read and never enter this bundle',
1100
+ 'foreign.skip.unsupported-agents': 'Subagent definitions in .claude/agents/*.md were not migrated: DSH subagents are registered at runtime by providers and have no file-level equivalent',
1101
+ 'foreign.skip.unsupported-rules': '.claude/rules/*.md files (with frontmatter scoping) were not migrated: merging them into the global instructions would drop their scope, so they are reported only',
1102
+ 'foreign.skip.unsupported-plugins': 'Claude Code plugin bodies were not migrated: their hooks and commands run inside the Claude runtime and cannot be loaded by DSH, and plugin code is never copied; skills and MCP definitions inside a plugin .mcp.json are migrated separately',
1103
+ 'foreign.skip.session-id-mismatch': 'The sessionId inside the session record does not match the file name: DSH locates sessions by directory and file name, so the file name wins and the recorded id is not used',
1104
+ 'foreign.skip.mcp-server-id-conflict': 'The same MCP server name appears in several places (global / project / plugin): the first one wins, and the other duplicates are reported but not migrated',
1105
+ 'foreign.skip.project-scan-truncated': 'Many projects were found: the project-level scan only covered the projects within the limit; the remaining projects were not read and their configuration is not in this bundle',
1084
1106
  // Per-item text for the three known lossy behaviours (F-5), rendered by
1085
1107
  // src/ui/foreign-view.ts; the card chrome keys live in src/client/locales.ts.
1086
1108
  'foreign.lossy.file-section-secrets.title': 'Plaintext in file-based sections is never stripped',
@@ -425,6 +425,12 @@ export interface RollbackSegmentResult {
425
425
  /** 无法安全撤销 ⇒ 未写(宿主回传才有) */
426
426
  unavailableCount?: number;
427
427
  unavailablePaths?: string[];
428
+ /**
429
+ * **落点无人认领**(时间窗证据表明那一刻没有任何会话活跃:手改 / 外部工具 / 另一个进程)
430
+ * ⇒ 未写(宿主回传才有)。与 unavailableCount 分开:成因不同,用户要做的事也不同。
431
+ */
432
+ externalCount?: number;
433
+ externalPaths?: string[];
428
434
  /** 写入前发现又被改过(并发)⇒ 未写(宿主回传才有) */
429
435
  concurrentCount?: number;
430
436
  concurrentPaths?: string[];
@@ -875,6 +881,12 @@ export interface RollbackRewindRowView {
875
881
  unavailableCount: number;
876
882
  /** 写入前发现又被改过(并发)未写的文件数 */
877
883
  concurrentCount: number;
884
+ /**
885
+ * **落点无人认领**未写的文件数 + 有界清单(2026-10-18):时间窗表明改这条路径的那一刻
886
+ * 没有任何会话活跃 ⇒ 手改 / 外部工具 / 另一个 DSH 进程。0 = 没有(或宿主没回传)。
887
+ */
888
+ unclaimedCount: number;
889
+ unclaimedPaths: string[];
878
890
  }
879
891
  export declare function rollbackRewindRows(result: RollbackRewindResult | null): RollbackRewindRowView[];
880
892
  /** 回退总体结论 → 字典键 + 徽章语义(未知回落 unknown,绝不裸渲染 outcome)。 */
@@ -737,6 +737,8 @@ export function rollbackRewindRows(result) {
737
737
  conflictPaths: redactLines(segment.conflictPaths),
738
738
  unavailableCount: numberOrNull(segment.unavailableCount) ?? 0,
739
739
  concurrentCount: numberOrNull(segment.concurrentCount) ?? 0,
740
+ unclaimedCount: numberOrNull(segment.externalCount) ?? 0,
741
+ unclaimedPaths: redactLines(segment.externalPaths),
740
742
  };
741
743
  });
742
744
  }
@@ -464,4 +464,51 @@ export declare function canRecloneWorkdir(workDir: {
464
464
  };
465
465
  error?: string;
466
466
  } | undefined): boolean;
467
+ /**
468
+ * 是否可以「采纳远端历史」(issue #104):前提与「重新克隆」相同(自检成功、目录里确实是 git
469
+ * 工作副本),区别只在破坏性 —— adopt 不删任何本地提交,可以 revert。
470
+ *
471
+ * 为什么**不**要求 workDir.match:issue #104 的触发路径恰恰是 mismatch(用户把仓库地址改成另一个
472
+ * 仓库后,自检看到 origin 仍指旧地址)。adopt 内部第一步就是 reconcileOrigin + fetch 当前配置地址,
473
+ * 所以 mismatch 正是该给这个入口的时候。地址还没跟上时先 relink 也行,但两键并不互斥。
474
+ */
475
+ export declare function canAdoptWorkdir(workDir: {
476
+ facts?: {
477
+ exists: boolean;
478
+ match: boolean;
479
+ };
480
+ error?: string;
481
+ } | undefined): boolean;
482
+ /**
483
+ * 工作副本的「积压」判定(issue #104 建议 3)。
484
+ *
485
+ * 要回答的是 issue 里那个拖了两周才暴露的问题:本地一直在攒提交,远端从来没收到过快照,
486
+ * 而界面没有任何一处说这件事。判定只用状态接口回传的两个数字(`ahead` / `remoteSnapshots`),
487
+ * 任何一个是 null(没走 detail 自检 / 无远端分支)就不猜 —— 返回 unknown,不渲染告警。
488
+ *
489
+ * - clean:没有未上传的提交;
490
+ * - never-synced:本地领先且远端零快照 ⇒ **醒目告警**(这条通道可能一直没在真正备份);
491
+ * - ahead:本地领先,但远端确实收到过快照(只是还没推完)。
492
+ */
493
+ export type WorkdirBacklog = {
494
+ kind: 'unknown';
495
+ } | {
496
+ kind: 'clean';
497
+ ahead: number;
498
+ remoteSnapshots: number;
499
+ } | {
500
+ kind: 'ahead';
501
+ ahead: number;
502
+ remoteSnapshots: number;
503
+ } | {
504
+ kind: 'never-synced';
505
+ ahead: number;
506
+ };
507
+ export declare function workdirBacklog(workDir: {
508
+ facts?: {
509
+ ahead?: number | null;
510
+ remoteSnapshots?: number | null;
511
+ };
512
+ error?: string;
513
+ } | undefined): WorkdirBacklog;
467
514
  export declare function formatSyncUrlPreview(url: string, max?: number): string;
@@ -538,6 +538,30 @@ export function needsWorkdirRelink(workDir) {
538
538
  export function canRecloneWorkdir(workDir) {
539
539
  return workDir !== undefined && workDir.error === undefined && workDir.facts?.exists === true;
540
540
  }
541
+ /**
542
+ * 是否可以「采纳远端历史」(issue #104):前提与「重新克隆」相同(自检成功、目录里确实是 git
543
+ * 工作副本),区别只在破坏性 —— adopt 不删任何本地提交,可以 revert。
544
+ *
545
+ * 为什么**不**要求 workDir.match:issue #104 的触发路径恰恰是 mismatch(用户把仓库地址改成另一个
546
+ * 仓库后,自检看到 origin 仍指旧地址)。adopt 内部第一步就是 reconcileOrigin + fetch 当前配置地址,
547
+ * 所以 mismatch 正是该给这个入口的时候。地址还没跟上时先 relink 也行,但两键并不互斥。
548
+ */
549
+ export function canAdoptWorkdir(workDir) {
550
+ return canRecloneWorkdir(workDir);
551
+ }
552
+ export function workdirBacklog(workDir) {
553
+ if (workDir === undefined || workDir.error !== undefined)
554
+ return { kind: 'unknown' };
555
+ const ahead = workDir.facts?.ahead;
556
+ const remoteSnapshots = workDir.facts?.remoteSnapshots;
557
+ if (typeof ahead !== 'number' || typeof remoteSnapshots !== 'number')
558
+ return { kind: 'unknown' };
559
+ if (ahead <= 0)
560
+ return { kind: 'clean', ahead, remoteSnapshots };
561
+ if (remoteSnapshots === 0)
562
+ return { kind: 'never-synced', ahead };
563
+ return { kind: 'ahead', ahead, remoteSnapshots };
564
+ }
541
565
  export function formatSyncUrlPreview(url, max = 60) {
542
566
  return url.slice(0, max);
543
567
  }
package/lib/ui/types.d.ts CHANGED
@@ -234,6 +234,22 @@ export interface RecoveryIncident {
234
234
  reason: string;
235
235
  createdAt: string;
236
236
  }
237
+ /**
238
+ * 正在**正常执行中**的操作(GET /recovery/status 的 `live`,加法字段)。
239
+ *
240
+ * 与 `incidents` 互斥:同一条 active journal 要么是「正在跑」(GLOBAL 锁由它持有、
241
+ * 且 journal.ownerInstanceId 与锁持有者一致),要么是「一次未正常完成的操作」。
242
+ * 判别只发生在宿主侧(single source = recovery-orchestrator 的 isLiveInFlight),
243
+ * 客户端只负责如实展示。
244
+ *
245
+ * 旧宿主不返回 → undefined(面板按「无进行中操作」处理)。
246
+ */
247
+ export interface RecoveryActiveOperation {
248
+ operationId: string;
249
+ operationType: string;
250
+ state: string;
251
+ startedAt: string;
252
+ }
237
253
  /** GET /recovery/status 响应。 */
238
254
  export interface RecoveryStatus {
239
255
  incidents: RecoveryIncident[];
@@ -241,6 +257,11 @@ export interface RecoveryStatus {
241
257
  runId: string;
242
258
  status: string;
243
259
  }[];
260
+ /**
261
+ * 正在正常执行的操作(不是 incident)。用途:面板要能解释「为什么现在什么都点不动」——
262
+ * 此前这类操作被误报成事故,「执行恢复」永久灰、放弃恢复一直 423,用户拿不到任何原因。
263
+ */
264
+ live?: RecoveryActiveOperation[];
244
265
  /**
245
266
  * 环境锁状态摘要(issue #31)。纯残留锁**不是** journal:进程在 op 期间被杀时
246
267
  * `journalId: null`、`transactions/active/` 为空,incidents 恒为 [],而 423 文案却
@@ -409,6 +409,7 @@ export declare class EnvironmentLockManager {
409
409
  state: LockState;
410
410
  detail?: string;
411
411
  corruptOwnership?: boolean;
412
+ ownerInstanceId?: string;
412
413
  }>;
413
414
  /**
414
415
  * 显式 stale recovery(独立动作;对应 CLI `--recover-stale-lock`)。
@@ -810,7 +810,9 @@ export class EnvironmentLockManager {
810
810
  const now = this.now();
811
811
  const heartbeatFresh = heartbeat !== null && (now - heartbeat.heartbeatAt) <= this.staleAfterMs;
812
812
  if (heartbeatFresh) {
813
- return { state: 'LOCKED', detail: `owner op=${rec.op} pid=${rec.owner.pid} (heartbeat fresh)` };
813
+ // ownerInstanceId 供**宿主内部**做 journal↔锁归属匹配(recovery status 的 in-flight 判定)。
814
+ // 它不是诊断文案:绝不进 HTTP 响应体(编排器只消费,不回传)。
815
+ return { state: 'LOCKED', ownerInstanceId: rec.owner.instanceId, detail: `owner op=${rec.op} pid=${rec.owner.pid} (heartbeat fresh)` };
814
816
  }
815
817
  // heartbeat 已过期:先探测 PID liveness(kill(pid,0) 跨平台可靠;ESRCH = 确证不存在)
816
818
  let ident;
@@ -822,7 +824,7 @@ export class EnvironmentLockManager {
822
824
  }
823
825
  // 确证死亡(ESRCH)→ STALE(不依赖 OS identity 能力)
824
826
  if (!ident.alive) {
825
- return { state: 'STALE_LOCK_DETECTED', detail: utilsMsg('utils.envLock.ownerConfirmedDead', { pid: rec.owner.pid }) };
827
+ return { state: 'STALE_LOCK_DETECTED', ownerInstanceId: rec.owner.instanceId, detail: utilsMsg('utils.envLock.ownerConfirmedDead', { pid: rec.owner.pid }) };
826
828
  }
827
829
  // PID 存活:需 OS identity 区分「reuse」与「同一进程 alive(heartbeat degraded)」
828
830
  // —— capability/值缺失 → 无法可靠确定 → UNKNOWN_STATE(保守拒删)
@@ -835,6 +837,7 @@ export class EnvironmentLockManager {
835
837
  if (longExpired !== null) {
836
838
  return {
837
839
  state: 'STALE_LOCK_DETECTED',
840
+ ownerInstanceId: rec.owner.instanceId,
838
841
  detail: utilsMsg('utils.envLock.ownerAliveUnverifiable', { pid: rec.owner.pid, longExpired }),
839
842
  };
840
843
  }
@@ -844,10 +847,10 @@ export class EnvironmentLockManager {
844
847
  };
845
848
  }
846
849
  if (rec.owner.osProcessStartIdentity !== ident.osProcessStartIdentity) {
847
- return { state: 'STALE_LOCK_DETECTED', detail: utilsMsg('utils.envLock.pidReuse', { pid: rec.owner.pid }) };
850
+ return { state: 'STALE_LOCK_DETECTED', ownerInstanceId: rec.owner.instanceId, detail: utilsMsg('utils.envLock.pidReuse', { pid: rec.owner.pid }) };
848
851
  }
849
852
  // alive 且 identity 相同 → owner alive(heartbeat degraded 保护)
850
- return { state: 'LOCKED', detail: utilsMsg('utils.envLock.ownerAliveDegraded') };
853
+ return { state: 'LOCKED', ownerInstanceId: rec.owner.instanceId, detail: utilsMsg('utils.envLock.ownerAliveDegraded') };
851
854
  }
852
855
  /* ------------------------------------------------------------ recover (explicit) */
853
856
  /**