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
@@ -165,7 +165,24 @@ export type ForeignSkipCode =
165
165
  * 该来源**没有自动根**(竞品 `defaultRoots.chatgpt = null`)—— 必须由用户显式给出导出文件
166
166
  * 路径;本机自动探测永远不会命中,如实报码而不是假装「未安装」。
167
167
  */
168
- | 'source-needs-explicit-path';
168
+ | 'source-needs-explicit-path'
169
+ /* ---- 2026-10-18:Claude Code 插件 / 项目级配置 / 权限 / 设置键的如实诊断(纯 additive) ---- */
170
+ /** settings.json 的 permissions(allow/ask/deny):DSH 无权限规则文件对等结构,只报告条数 */
171
+ | 'unsupported-permissions'
172
+ /** settings.json 里其余未迁移的顶层键(只记键名与条数,**值一律不读**) */
173
+ | 'unsupported-settings-keys'
174
+ /** .claude/agents/*.md 子代理定义:DSH 的 subagent 是运行时 provider 注册制,无文件级对等结构 */
175
+ | 'unsupported-agents'
176
+ /** .claude/rules/*.md 带 frontmatter 作用域:合并进全局指令会丢作用域,故只报告 */
177
+ | 'unsupported-rules'
178
+ /** Claude Code 插件本体(插件 API / hooks / commands 的执行面):DSH 无法加载,**绝不复制其代码** */
179
+ | 'unsupported-plugins'
180
+ /** 会话记录里的 sessionId 与文件名 stem 不一致(DSH 以目录/文件名定位会话,故以文件名为准并如实报码) */
181
+ | 'session-id-mismatch'
182
+ /** 同名 MCP server 在多处出现(全局 / 项目级 / 插件):先到先得,其余只报告 */
183
+ | 'mcp-server-id-conflict'
184
+ /** 项目级扫描触顶(maxProjects):**不静默**,如实报码 */
185
+ | 'project-scan-truncated';
169
186
 
170
187
  /**
171
188
  * 冲突语义(**契约冻结**,§8.4):同 id 会话恒「不覆盖、跳过并报码」。
@@ -201,7 +218,7 @@ export interface ClaudeCodeInput {
201
218
  skills?: ClaudeSkillInput[];
202
219
  /** ~/.claude/CLAUDE.md 原文(全局指令) */
203
220
  memory?: string;
204
- /** ~/.claude/commands/* 的普通文件数(本期只报不搬) */
221
+ /** ~/.claude/commands/** 的普通文件数(**递归**;只报不搬) */
205
222
  commandCount?: number;
206
223
  /** ~/.claude/projects/<项目>/<uuid>.jsonl 的原文(转码在翻译层做) */
207
224
  sessions?: ClaudeSessionInput[];
@@ -210,6 +227,94 @@ export interface ClaudeCodeInput {
210
227
  * 缺省 = 不转码任何会话并逐条报 session-format-version-unknown,绝不猜版本。
211
228
  */
212
229
  targetSessionFormatVersion?: number;
230
+ /**
231
+ * 实际使用的 Claude 配置目录绝对路径(CLAUDE_CONFIG_DIR 覆盖时为该值,否则 <home>/.claude)。
232
+ * **只进溯源元数据**,不进任何分区载荷。
233
+ */
234
+ configDir?: string;
235
+ /** 位置被 CLAUDE_CONFIG_DIR 覆盖(命中即报 source-location-overridden) */
236
+ configDirOverridden?: boolean;
237
+ /** 来源侧版本标记(~/.claude.json 的 lastVersionBase 等;拿不到即省略) */
238
+ sourceVersion?: string;
239
+ /** 项目级配置的已读形态(项目根来自 ~/.claude.json 的 projects 键;**不猜路径**) */
240
+ projects?: ClaudeProjectInput[];
241
+ /** 用户级 Claude Code 插件的已读形态(只读元数据与组件,**绝不执行插件代码**) */
242
+ plugins?: ClaudePluginInput[];
243
+ /** 已登记的市场(name + 来源仓库;URL 的 userinfo 已剥离) */
244
+ marketplaces?: { name: string; repo?: string }[];
245
+ }
246
+
247
+ /**
248
+ * 一个 Claude Code **项目级**配置目录的已读形态(纯数据;翻译层不做任何 fs 访问)。
249
+ *
250
+ * 位置口径(实测取证 2026-10-18,本机 ~/.claude.json 的 projects 键):项目根来自
251
+ * `~/.claude.json` 的 `projects` 键(绝对路径,Claude Code 自己写的权威清单)——
252
+ * **不按目录名猜项目路径**(猜 = 把会话归到不存在的目录)。
253
+ */
254
+ export interface ClaudeProjectInput {
255
+ /** 项目根绝对路径(**仅进溯源元数据与报码,不进任何分区载荷**) */
256
+ root: string;
257
+ /** `<root>/.claude` 是否存在(false = 该项目没有 Claude 配置,读盘层不产出任何 finding) */
258
+ hasClaudeDir?: boolean;
259
+ /** `<root>/.claude/settings.json`(已解析) */
260
+ settings?: unknown;
261
+ /** `<root>/.claude/settings.local.json`(已解析;值一律不读,只做诊断) */
262
+ settingsLocal?: unknown;
263
+ /** `<root>/CLAUDE.md` 原文(项目级指令) */
264
+ memory?: string;
265
+ /** `<root>/CLAUDE.local.md` 原文(本地覆盖指令) */
266
+ memoryLocal?: string;
267
+ /** `<root>/.claude/rules/*.md`:文件名 + 原文(带 frontmatter 的按 unsupported-rules 只报告) */
268
+ rules?: { name: string; text: string }[];
269
+ /** `<root>/.mcp.json`(已解析;Claude Code 的项目级 MCP 载体) */
270
+ mcpJson?: unknown;
271
+ /** `~/.claude.json` → `projects[<root>].mcpServers`(已解析) */
272
+ projectMcp?: unknown;
273
+ /** `<root>/.claude/skills/*` 的单元 */
274
+ skills?: ClaudeSkillInput[];
275
+ /** `<root>/.claude/commands/**` 的普通文件数(**递归**;只报不搬) */
276
+ commandCount?: number;
277
+ /** `<root>/.claude/agents/*.md`:名字 + 文件名(只报告,无文件级对等结构) */
278
+ agents?: { name: string; file: string }[];
279
+ /** 读盘层发现的问题(读不到 / 超限 / 非法)——由翻译层原样带出 */
280
+ readFindings?: ForeignSkip[];
281
+ }
282
+
283
+ /**
284
+ * 一个 Claude Code **插件**的已读形态(纯数据)。
285
+ *
286
+ * 三条结构性保证:
287
+ * ① **只读元数据与组件,绝不执行插件代码**(Claude 插件的 hooks/commands 是 JS 执行面);
288
+ * ② 安装位置/来源只进**溯源元数据**(bundle 的 foreign 块),不由本层写进任何分区;
289
+ * ③ 能无损复用的组件只有 skills 与 .mcp.json 里的 MCP 定义(凭据先剥离),其余一律报码。
290
+ */
291
+ export interface ClaudePluginInput {
292
+ /** 插件名(plugin.json 的 name;缺失时回落目录名) */
293
+ name: string;
294
+ /** 插件版本(plugin.json 的 version;缺失即省略) */
295
+ version?: string;
296
+ /** 来源市场名(known_marketplaces.json / marketplace.json 的键) */
297
+ marketplace?: string;
298
+ /** 来源仓库(owner/repo 或 URL;**已剥离 userinfo,绝不含凭据**) */
299
+ repo?: string;
300
+ /** 来源 ref / sha(若有) */
301
+ ref?: string;
302
+ /** 安装位置(相对 ~/.claude/plugins 的 POSIX 相对路径;进溯源用) */
303
+ installRel?: string;
304
+ /** 实际存在的组件种类(skills / commands / agents / hooks / mcp),供诊断文案用 */
305
+ components: string[];
306
+ /** 插件自带的 skills(name = 压平后的技能名) */
307
+ skills?: ClaudeSkillInput[];
308
+ /** 插件自带 `.mcp.json`(已解析) */
309
+ mcpJson?: unknown;
310
+ /** 插件自带 commands 的文件数(**递归**;只报不搬) */
311
+ commandCount?: number;
312
+ /** 插件自带 agents 的文件名(只报告) */
313
+ agents?: string[];
314
+ /** 插件自带 hooks 的文件数(只报告;绝不只是不执行) */
315
+ hookFiles?: number;
316
+ /** 读盘层发现的问题 */
317
+ readFindings?: ForeignSkip[];
213
318
  }
214
319
 
215
320
  /** 一个待转码的 Claude Code 会话文件 */
@@ -217,6 +322,19 @@ export interface ClaudeSessionInput {
217
322
  /** 文件名(不含 .jsonl);同时作为 DSH 侧会话 id */
218
323
  id: string;
219
324
  text: string;
325
+ /**
326
+ * 记录里首个出现的 sessionId。与 id(文件名 stem)不一致时由翻译层报 session-id-mismatch:
327
+ * DSH 以「目录名/文件名」定位会话,故仍以文件名为准,但**不静默**。
328
+ */
329
+ recordSessionId?: string;
330
+ /**
331
+ * 确定性 createdAt 回落值(毫秒):读盘层给「最早可解析记录时间」,没有时间戳时给文件 mtime。
332
+ *
333
+ * 为什么必须由读盘层给:DSH 侧「同内容 → Skip」的判定是**逐字节**的,而会话产物里带着
334
+ * createdAt/updatedAt;若回落用 Date.now(),同一来源转码两次字节必然不同 ⇒ 重复导入恒判
335
+ * Conflict 而不是 Skip(实测缺陷)。由文件自身 mtime 回落即可让转码**幂等**。
336
+ */
337
+ createdAtMs?: number;
220
338
  }
221
339
 
222
340
  /* ---------------- Hermes(~/.hermes,Windows 为 %LOCALAPPDATA%\Hermes) ---------------- */
@@ -309,4 +427,46 @@ export interface ForeignImportResult {
309
427
  */
310
428
  credentialRefs: string[];
311
429
  counts: Record<string, number>;
430
+ /**
431
+ * 溯源块(进 bundle 的 manifest.foreign;缺省 = 不写 —— 旧行为逐字不变)。
432
+ * 由**翻译层**装配(只有它同时看得到位置、项目根与插件清单)。
433
+ */
434
+ origin?: ForeignOrigin;
435
+ }
436
+
437
+ /**
438
+ * 外部来源的**溯源块**(进 bundle 的 manifest.foreign,见 src/schema/types.ts)。
439
+ *
440
+ * 为什么必须有:标准 bundle v1 的 manifest 原本没有任何「这包从哪来」的字段,落盘后
441
+ * 包与 DSH 自产备份**逐字同构**(实测缺陷)—— 事后无法复核来源、兼容性评分也因
442
+ * source.dshVersion 被填成目标机版本而被构造性拉满。
443
+ *
444
+ * 三条纪律:
445
+ * ① **只放标识与计数,绝不放值**(凭据引用只有名字;错误只有机器码);
446
+ * ② 绝对路径只允许出现在 projects[] 与 installPath 这类「用户自己机器的来源位置」上,
447
+ * 且**不得**写进任何分区载荷;
448
+ * ③ 缺字段即「不知道」,绝不填默认值假装知道。
449
+ */
450
+ export interface ForeignOrigin {
451
+ /** 来源 agent id(ForeignSourceId 的运行期字面量) */
452
+ source: string;
453
+ /** 实际读取的位置(相对 home 的 POSIX 路径;不含任何值) */
454
+ locations: string[];
455
+ /** 来源侧版本标记(如 Claude Code `~/.claude.json` 的 lastVersionBase;读不到即省略) */
456
+ sourceVersion?: string;
457
+ /** 被扫描的项目根绝对路径(仅溯源,供用户核对) */
458
+ projects?: string[];
459
+ /** 插件清单(名称 / 版本 / 市场 / 来源仓库 / ref / 组件种类;**不含凭据**) */
460
+ plugins?: {
461
+ name: string;
462
+ version?: string;
463
+ marketplace?: string;
464
+ repo?: string;
465
+ ref?: string;
466
+ components?: string[];
467
+ }[];
468
+ /** 跳过与诊断汇总(机器码 + 条数,**不含任何值**) */
469
+ skipped?: { code: string; count: number }[];
470
+ /** 需要用户在目标机补录的凭据引用名(只有名字) */
471
+ credentialRefs?: string[];
312
472
  }
@@ -140,7 +140,7 @@ test('install: bundle 包成功 → 不补 patch 行(reconcile 维护 bundles
140
140
  }
141
141
  });
142
142
 
143
- test('install: 非 registry spec(github:)→ add 按 spec 安装;registry 版本区间 → 裸包名(npm 最新)', async () => {
143
+ test('install: 非 registry spec(github:)原样安装;registry 区间按「包名@spec」安装(issue #101)', async () => {
144
144
  const { homeDir, profileDir, cleanup } = makeTempProfile();
145
145
  try {
146
146
  writeInstalledPkg(profileDir, 'dsh-memory-evolve', '1.0.0');
@@ -153,13 +153,35 @@ test('install: 非 registry spec(github:)→ add 按 spec 安装;registry
153
153
  const facade = new DshPluginsFacade(homeDir, 'web', new MemPatchFile(), runner);
154
154
 
155
155
  await facade.install('dsh-memory-evolve', 'github:csyangwen/dsh-memory-evolve');
156
- await facade.install('dshmarket', '^1.0.3');
156
+ await facade.install('dshmarket', 'dshmarket@^1.0.3');
157
157
  await facade.install('pkg-plain');
158
158
  assert.deepEqual(calls.map((c) => c.args), [
159
159
  ['add', 'github:csyangwen/dsh-memory-evolve'],
160
- ['add', 'dshmarket'],
160
+ ['add', 'dshmarket@^1.0.3'],
161
161
  ['add', 'pkg-plain'],
162
- ], 'github: 来源按 spec 安装;registry 版本区间与裸包名都走 npm 最新版');
162
+ ], 'github: 来源原样安装;registry 区间带包名(裸包名会被目标机既有 spec 挡住,issue #101);缺省仍退化为裸包名');
163
+ } finally {
164
+ cleanup();
165
+ }
166
+ });
167
+
168
+ test('install: 精确版本 target 原样执行,并回读 node_modules 实际版本(issue #101 落地校验依据)', async () => {
169
+ const { homeDir, profileDir, cleanup } = makeTempProfile();
170
+ try {
171
+ writeInstalledPkg(profileDir, 'dsh-free-search', '0.9.3');
172
+ const calls: { args: string[] }[] = [];
173
+ const runner = async (_p: string, _profile: string, args: readonly string[]): Promise<DshPluginResult> => {
174
+ calls.push({ args: [...args] });
175
+ // 模拟 pnpm 真的按 target 装了备份记录的版本
176
+ writeInstalledPkg(profileDir, 'dsh-free-search', '0.9.5');
177
+ return okResult();
178
+ };
179
+ const facade = new DshPluginsFacade(homeDir, 'web', new MemPatchFile(), runner);
180
+
181
+ const r = await facade.install('dsh-free-search', 'dsh-free-search@0.9.5');
182
+ assert.deepEqual(calls[0]?.args, ['add', 'dsh-free-search@0.9.5'], '精确版本必须进 argv,不能退化成裸包名');
183
+ assert.equal(r.needsRestart, true);
184
+ assert.equal(r.installedVersion, '0.9.5', '回读安装后的真实版本,供 adapter 校验是否真的落地');
163
185
  } finally {
164
186
  cleanup();
165
187
  }
package/src/index.ts CHANGED
@@ -81,13 +81,14 @@ import { msgOf, zhMsg } from './core/messages.ts'
81
81
  import { engineMsg, getEngineLocale, setEngineLanguageSource, setEngineLocale } from './core/engine-locale.ts'
82
82
  import { RollbackAutoTracker, RollbackPointEngine, ROLLBACK_POINTS_DIR_NAME, rollbackSessionBindingOf, rollbackTrackedEventOf } from './core/rollback-point.ts'
83
83
  import { ROLLBACK_ATTRIBUTION_FILE_NAME, RollbackWriteAttribution } from './core/rollback-attribution.ts'
84
+ import { ROLLBACK_ACTIVITY_FILE_NAME, RollbackSessionActivity } from './core/rollback-activity.ts'
84
85
  import { isRollbackEnabled, onRollbackEnabledChange, setRollbackEnabled } from './core/rollback-gate.ts'
85
86
  import type { RollbackForkCarrier, RollbackRewindResult } from './core/rollback-point.ts'
86
87
  // t3:宿主斜杠命令 /rollback(与 HTTP 路由同一道写入门;命令服务惰取,未组合时安静跳过)。
87
88
  import { agentSessionFactsOf, registerRollbackCommand } from './core/command-rollback.ts'
88
89
  import type { CommandServiceLike, RollbackCommandHost } from './core/command-rollback.ts'
89
90
  import type { MsgFunc } from './core/messages.ts'
90
- import { cleanupAbortedInstall, hasDshBundlePatch, installAnchorFromProfileContext, installErrorFor, installSpecFor, listInstalledPlugins, profileNameFromProfileContext, resolveProcessProfileName, resolveProfileDir, readProfileManifest, runDshPlugin, validateProfileName } from './core/plugin-cli.ts'
91
+ import { cleanupAbortedInstall, hasDshBundlePatch, installAnchorFromProfileContext, installErrorFor, readInstalledVersion, listInstalledPlugins, profileNameFromProfileContext, resolveProcessProfileName, resolveProfileDir, readProfileManifest, runDshPlugin, validateProfileName } from './core/plugin-cli.ts'
91
92
  import type { ConfigAdapter, CredentialsFacade, ExportUnit, FileSystemFacade, HostContext, ImportDecisions, ImportPlan, NamedNamespaceInfo, NamespaceInfo, PatchFileFacade, PlanItemKind, PluginInfo, PluginsFacade, SessionMoveResult, SessionParentRelation, SessionRewriteResult, SessionStoreFacade, SettingsFacade, SkillDefinitionView, SkillStoreFacade, SkillSummaryView, WorkspaceFacade } from './core/types.ts'
92
93
  import { ImportUserSkippedError } from './core/types.ts'
93
94
  import { PLUGIN_NPM_PACKAGE } from './core/update-check.ts'
@@ -132,8 +133,13 @@ import { DEFAULT_RETENTION_POLICY, selectPruneCandidatesByPolicy } from './sync/
132
133
  import type { RetentionPolicy } from './sync/retention-policy.ts'
133
134
  import type { PruneSelector } from './core/backup.ts'
134
135
  import { selectPruneCandidates } from './core/backup.ts'
135
- import { readAllAutosyncConfigs, readAutosyncConfig } from './sync/autosync-config.ts'
136
- import type { AutosyncInterval, AutosyncRunStatus } from './sync/autosync-config.ts'
136
+ import {
137
+ readAllAutosyncConfigs, readAutosyncConfig, DEFAULT_AUTOSYNC_REALTIME, DEFAULT_AUTOSYNC_REMOTE_CHECK_MS,
138
+ DEFAULT_AUTOSYNC_AUTO_APPLY_TIER, DEFAULT_AUTOSYNC_CONFLICT_POLICY, lastRunFailureMessage,
139
+ } from './sync/autosync-config.ts'
140
+ import type {
141
+ AutosyncInterval, AutosyncRunStatus, AutosyncAutoApplyTier, AutosyncConflictPolicy,
142
+ } from './sync/autosync-config.ts'
137
143
  import { appendAutosyncEntry } from './sync/sync-history.ts'
138
144
  import { MigrationStore, type MigrationKind, type MigrationResult, MIGRATION_HISTORY_DIR } from './core/migration-history.ts'
139
145
  import { readSyncConfig, validateRepoUrl, validateWebDavUrl, isWebDavConfig, isS3Config, isGistConfig, channelOf, parseSyncChannel, SYNC_CHANNELS, GIST_PROVIDER, isS3CompatProvider, validateCloudChannelInput, cloudSecretRef } from './sync/sync-config.ts'
@@ -181,7 +187,7 @@ export const name = 'config-manager'
181
187
  export const inject = ['settings', 'credentials']
182
188
 
183
189
  /** Plugin version, kept in sync with package.json ("version"). */
184
- export const PLUGIN_VERSION = '0.1.78'
190
+ export const PLUGIN_VERSION = '0.1.80'
185
191
 
186
192
  /** Plugin own package name — excluded from its own exported plugins list. */
187
193
  const PLUGIN_NAME = 'dsh-config-manager'
@@ -246,7 +252,7 @@ export interface Config {
246
252
  /**
247
253
  * 本文件保留的 7 条路由的路径(被源码级守卫按文件窗口钉住,见下方 routesList 注释)。
248
254
  *
249
- * 其余 72 条路由的路径**声明在各自的组文件里**(src/routes/*.ts 的 endpoint({ path, methods }))——
255
+ * 其余 75 条路由的路径**声明在各自的组文件里**(src/routes/*.ts 的 endpoint({ path, methods }))——
250
256
  * 新增一条 API 只需那一条声明,不再有「常量表 + 路由对象」两处要同步。
251
257
  */
252
258
  export const API = {
@@ -649,12 +655,13 @@ export class DshPluginsFacade implements PluginsFacade {
649
655
  return listInstalledPlugins(this.homeDir, this.profile)
650
656
  }
651
657
 
652
- async install(pkg: string, spec?: string, signal?: AbortSignal): Promise<{ needsRestart: boolean }> {
658
+ async install(pkg: string, target?: string, signal?: AbortSignal): Promise<{ needsRestart: boolean; installedVersion?: string | null }> {
653
659
  const profileDir = resolveProfileDir(this.homeDir, this.profile)
654
- // 非 registry 来源(github:/git+/file: 等)按来源 spec 安装;registry 包按裸包名装
655
- // npm 最新版(官方机制)。spec 丢失(旧备份)时退化为裸包名 → pnpm fetch-404,
656
- // 由 installErrorFor 给出可操作诊断。
657
- const result = await this.runner(profileDir, this.profile, ['add', installSpecFor(pkg, spec)], undefined, signal)
660
+ // target 由调用方(adapters/plugins.ts 的 pluginInstallTarget)推导完毕,这里只执行:
661
+ // registry 包带精确版本或「包名@spec」,pnpm 才不会静默沿用目标机既有 spec(issue #101)。
662
+ // 缺省退化为裸包名(旧调用方)→ pnpm fetch-404,由 installErrorFor 给出可操作诊断。
663
+ const spec = target !== undefined && target !== '' ? target : pkg
664
+ const result = await this.runner(profileDir, this.profile, ['add', spec], undefined, signal)
658
665
  // 用户「跳过当前插件」:宿主 kill 了子进程 → 清理半装状态(删依赖行 + 删 node_modules/<pkg>,
659
666
  // 防止「package.json 声明了依赖但没装全」导致 DSH 启动失败),再以跳过语义抛错。
660
667
  if (result.aborted || (signal !== undefined && signal.aborted)) {
@@ -670,7 +677,8 @@ export class DshPluginsFacade implements PluginsFacade {
670
677
  const reason = error instanceof Error ? error.message : String(error)
671
678
  throw new Error(this.msg('host.activationRowFailed', { pkg, reason }))
672
679
  }
673
- return { needsRestart: true }
680
+ // 回读实际落盘版本:调用方据此校验升级是否真的生效(issue #101),而不是只看子进程退出码。
681
+ return { needsRestart: true, installedVersion: readInstalledVersion(profileDir, pkg) }
674
682
  }
675
683
  }
676
684
 
@@ -2307,7 +2315,7 @@ export async function tryDecryptCredentials(
2307
2315
  * (见 W1 报告的重测配方),把结果落到这里;makeRoutes 里的 const routeEnv: RouteEnvInferred =
2308
2316
  * 注解保证两侧不漂移(新增依赖漏登记会在构造点报错)。
2309
2317
  */
2310
- export type RouteEnvInferred = { sessionExportFetch: ((input: string, init: RequestInit) => Promise<Response>) | undefined; sessionExportPort: number | undefined; adapters: ConfigAdapter<unknown>[]; sessionHealth: { homeDir: string; targetFormatVersion: () => number | undefined; workspaceKeys: () => Promise<ReadonlySet<string>>; knownSessionIds: () => Promise<ReadonlySet<string>>; sessionMeta: (ids: readonly string[]) => Promise<SessionMetaIndex>; }; backupScheduler: BackupScheduler; bootSafetyAudit: () => Promise<BootSafetyReport>; cancelDecisionTimeoutMs: number; buildMarketSummary: (e: { url: string; addedAt: string; }) => Promise<MarketSummary>; credentials: CredentialProvider; dataDir: string; exportsDir: string; githubAuth: GitHubAuthClient; githubClientId: string | undefined; githubClientSecret: string | undefined; githubFlows: DeviceFlowStore; history: MigrationStore; host: ConfigManagerHostContext; itemCached: (url: string, itemId: string) => Promise<boolean>; knownSyncSectionIds: Set<SectionId>; makeImporter: () => Importer; makeMarketReader: () => GitMarketReader; makeRecoveryExecutors: (runId: string) => RecoveryExecutorFns; makeSyncEngine: (cfg: SyncConfig, engineOpts?: { optInSections?: readonly SectionId[]; }) => SyncEngine; marketBootAutoRefreshed: { value: boolean; }; marketCacheIndex: (url: string) => string; marketDir: string; marketCacheItemDir: (url: string) => string; marketStarCache: StarCache; marketWorkDir: (url: string) => string; meGitHubRest: GitHubAuthRest; meService: MyRepoService; meTokenProvider: () => Promise<string>; msg: MsgFunc; prepareSync: (body: Record<string, unknown>) => Promise<SyncConfig>; profileLauncher: DshProfileLauncher; profileRuntime: DshProfileRuntimeRegistry; profiles: DshProfileManager; pruneStagedMarketZips: () => Promise<void>; readCachedIndexObj: (url: string) => Promise<MarketIndex | null>; recoveryOrchestrator: RecoveryOrchestrator; resolveSyncPassword: (ref: string) => Promise<string | undefined>; roots: string[]; runAbortControllers: Map<string, AbortController>; runCancels: Map<string, { signal: AbortController; settle: (d: 'rollback' | 'keep') => void; decided: boolean }>; runs: RunRegistry; scheduler: AutoSyncScheduler; selectionCache: Partial<Record<SyncTransportType, SyncSelection>>; selectionOptInSections: (channel: SyncTransportType, onlyAuto?: boolean) => SectionId[]; selectionView: (channel: SyncTransportType) => Promise<SelectionView>; selectionViewByChannel: () => Promise<Record<SyncTransportType, SelectionView>>; snapshotEntrySections: (snapshotDir: string) => Promise<string[]>; snapshotsDir: string; syncCredentialsByChannelView: () => Promise<Record<SyncTransportType, { encryptPasswordConfigured: boolean; decryptPasswordConfigured: boolean; }>>; syncDir: string; syncPasswordConfigured: (ref: string) => Promise<boolean>; syncWorkDir: { inspect: (repoUrl: string) => Promise<{ facts: GitWorkDirFacts; } | { error: string; }>; rebuild: (repoUrl: string, mode: 'relink' | 'reclone') => Promise<GitWorkDirFacts>; }; syncSectionCatalog: { id: SectionId; displayName: string; portability: Portability; defaultIncluded: boolean; }[]; syncSessions: SyncSessionStore; tmpDir: string; tryAppendHistory: (raw: { kind: MigrationKind; result: MigrationResult; sections: string[]; operationId?: string; snapshotId?: string; runId?: string; source: 'api' | 'autosync' | 'backup-scheduler' | 'recovery' | 'cli' | 'internal'; summary: string; error?: string; }) => Promise<string | undefined>; withMutationGate: (op: string, handler: (req: IncomingMessage, res: ServerResponse, lockCtx?: MutationLockContext, journalCtx?: JournalRunContext) => Promise<void>, opts?: { journaled?: boolean; deferredSnapshot?: boolean; }) => ((req: IncomingMessage, res: ServerResponse) => Promise<void>); selfUpdate: (targetVersion: string) => Promise<SelfUpdateOutcome>; writeItemCache: (url: string, itemId: string, manifestRaw: string, zipBytes: Uint8Array) => Promise<void>; }
2318
+ export type RouteEnvInferred = { sessionExportFetch: ((input: string, init: RequestInit) => Promise<Response>) | undefined; sessionExportPort: number | undefined; adapters: ConfigAdapter<unknown>[]; sessionHealth: { homeDir: string; targetFormatVersion: () => number | undefined; workspaceKeys: () => Promise<ReadonlySet<string>>; knownSessionIds: () => Promise<ReadonlySet<string>>; sessionMeta: (ids: readonly string[]) => Promise<SessionMetaIndex>; }; backupScheduler: BackupScheduler; bootSafetyAudit: () => Promise<BootSafetyReport>; cancelDecisionTimeoutMs: number; buildMarketSummary: (e: { url: string; addedAt: string; }) => Promise<MarketSummary>; credentials: CredentialProvider; dataDir: string; exportsDir: string; githubAuth: GitHubAuthClient; githubClientId: string | undefined; githubClientSecret: string | undefined; githubFlows: DeviceFlowStore; history: MigrationStore; host: ConfigManagerHostContext; itemCached: (url: string, itemId: string) => Promise<boolean>; knownSyncSectionIds: Set<SectionId>; makeImporter: () => Importer; makeMarketReader: () => GitMarketReader; makeRecoveryExecutors: (runId: string) => RecoveryExecutorFns; makeSyncEngine: (cfg: SyncConfig, engineOpts?: { optInSections?: readonly SectionId[]; }) => SyncEngine; marketBootAutoRefreshed: { value: boolean; }; marketCacheIndex: (url: string) => string; marketDir: string; marketCacheItemDir: (url: string) => string; marketStarCache: StarCache; marketWorkDir: (url: string) => string; meGitHubRest: GitHubAuthRest; meService: MyRepoService; meTokenProvider: () => Promise<string>; msg: MsgFunc; prepareSync: (body: Record<string, unknown>) => Promise<SyncConfig>; profileLauncher: DshProfileLauncher; profileRuntime: DshProfileRuntimeRegistry; profiles: DshProfileManager; pruneStagedMarketZips: () => Promise<void>; readCachedIndexObj: (url: string) => Promise<MarketIndex | null>; recoveryOrchestrator: RecoveryOrchestrator; resolveSyncPassword: (ref: string) => Promise<string | undefined>; roots: string[]; runAbortControllers: Map<string, AbortController>; runCancels: Map<string, { signal: AbortController; settle: (d: 'rollback' | 'keep') => void; decided: boolean }>; runs: RunRegistry; scheduler: AutoSyncScheduler; selectionCache: Partial<Record<SyncTransportType, SyncSelection>>; selectionOptInSections: (channel: SyncTransportType, onlyAuto?: boolean) => SectionId[]; selectionView: (channel: SyncTransportType) => Promise<SelectionView>; selectionViewByChannel: () => Promise<Record<SyncTransportType, SelectionView>>; snapshotEntrySections: (snapshotDir: string) => Promise<string[]>; snapshotsDir: string; syncCredentialsByChannelView: () => Promise<Record<SyncTransportType, { encryptPasswordConfigured: boolean; decryptPasswordConfigured: boolean; }>>; syncDir: string; syncPasswordConfigured: (ref: string) => Promise<boolean>; syncWorkDir: { inspect: (repoUrl: string) => Promise<{ facts: GitWorkDirFacts; } | { error: string; }>; rebuild: (repoUrl: string, mode: 'relink' | 'adopt' | 'reclone') => Promise<GitWorkDirFacts>; }; syncSectionCatalog: { id: SectionId; displayName: string; portability: Portability; defaultIncluded: boolean; }[]; syncSessions: SyncSessionStore; tmpDir: string; tryAppendHistory: (raw: { kind: MigrationKind; result: MigrationResult; sections: string[]; operationId?: string; snapshotId?: string; runId?: string; source: 'api' | 'autosync' | 'backup-scheduler' | 'recovery' | 'cli' | 'internal'; summary: string; error?: string; }) => Promise<string | undefined>; withMutationGate: (op: string, handler: (req: IncomingMessage, res: ServerResponse, lockCtx?: MutationLockContext, journalCtx?: JournalRunContext) => Promise<void>, opts?: { journaled?: boolean; deferredSnapshot?: boolean; }) => ((req: IncomingMessage, res: ServerResponse) => Promise<void>); selfUpdate: (targetVersion: string) => Promise<SelfUpdateOutcome>; writeItemCache: (url: string, itemId: string, manifestRaw: string, zipBytes: Uint8Array) => Promise<void>; }
2311
2319
 
2312
2320
  /** 解密错误 → 用户可读文本:BAD_PASSWORD 只报「密码错误」(不泄内部细节),其余原文 */
2313
2321
  export function decryptErrorText(error: unknown, msg: MsgFunc): string {
@@ -2694,12 +2702,19 @@ export async function buildAutosyncStatus(dir: string, channel: SyncTransportTyp
2694
2702
  const elapsedMs = cfg.lastRunAt === undefined || cfg.lastRunAt === ''
2695
2703
  ? -1
2696
2704
  : Math.max(0, Date.now() - Date.parse(cfg.lastRunAt))
2705
+ // issue #105:只有本轮真的失败才回传错误原文(success/skipped/partial 不得带上一轮的旧错误)
2706
+ const failureMessage = lastRunFailureMessage(cfg)
2697
2707
  return {
2698
2708
  enabled: cfg.enabled,
2699
2709
  interval: cfg.interval,
2710
+ // 真·自动同步三件套(缺省值在读取层已补齐,这里回传「生效中的值」)
2711
+ realtime: cfg.realtime ?? DEFAULT_AUTOSYNC_REALTIME,
2712
+ remoteCheckMs: cfg.remoteCheckMs ?? DEFAULT_AUTOSYNC_REMOTE_CHECK_MS,
2713
+ autoApplyTier: cfg.autoApplyTier ?? DEFAULT_AUTOSYNC_AUTO_APPLY_TIER,
2714
+ conflictPolicy: cfg.conflictPolicy ?? DEFAULT_AUTOSYNC_CONFLICT_POLICY,
2700
2715
  ...(cfg.lastRunAt !== undefined ? { lastRunAt: cfg.lastRunAt } : {}),
2701
2716
  ...(cfg.lastRunStatus !== undefined ? { lastRunStatus: cfg.lastRunStatus } : {}),
2702
- ...(cfg.lastRunMessage !== undefined ? { lastRunMessage: cfg.lastRunMessage } : {}),
2717
+ ...(failureMessage !== undefined ? { lastRunMessage: failureMessage } : {}),
2703
2718
  // issue #79:跳过原因以**码**回传(界面本地化);不再只留在 info 级日志里
2704
2719
  ...(cfg.lastRunSkipReason !== undefined ? { lastRunSkipReason: cfg.lastRunSkipReason } : {}),
2705
2720
  ...(cfg.lastRunSkipCount !== undefined ? { lastRunSkipCount: cfg.lastRunSkipCount } : {}),
@@ -2717,12 +2732,18 @@ export async function buildAutosyncStatusByChannel(dir: string): Promise<Record<
2717
2732
  const elapsedMs = cfg.lastRunAt === undefined || cfg.lastRunAt === ''
2718
2733
  ? -1
2719
2734
  : Math.max(0, Date.now() - Date.parse(cfg.lastRunAt))
2735
+ // issue #105:同 buildAutosyncStatus —— 只有本轮失败才带错误原文
2736
+ const failureMessage = lastRunFailureMessage(cfg)
2720
2737
  return {
2721
2738
  enabled: cfg.enabled,
2722
2739
  interval: cfg.interval,
2740
+ realtime: cfg.realtime ?? DEFAULT_AUTOSYNC_REALTIME,
2741
+ remoteCheckMs: cfg.remoteCheckMs ?? DEFAULT_AUTOSYNC_REMOTE_CHECK_MS,
2742
+ autoApplyTier: cfg.autoApplyTier ?? DEFAULT_AUTOSYNC_AUTO_APPLY_TIER,
2743
+ conflictPolicy: cfg.conflictPolicy ?? DEFAULT_AUTOSYNC_CONFLICT_POLICY,
2723
2744
  ...(cfg.lastRunAt !== undefined ? { lastRunAt: cfg.lastRunAt } : {}),
2724
2745
  ...(cfg.lastRunStatus !== undefined ? { lastRunStatus: cfg.lastRunStatus } : {}),
2725
- ...(cfg.lastRunMessage !== undefined ? { lastRunMessage: cfg.lastRunMessage } : {}),
2746
+ ...(failureMessage !== undefined ? { lastRunMessage: failureMessage } : {}),
2726
2747
  ...(cfg.lastRunSkipReason !== undefined ? { lastRunSkipReason: cfg.lastRunSkipReason } : {}),
2727
2748
  ...(cfg.lastRunSkipCount !== undefined ? { lastRunSkipCount: cfg.lastRunSkipCount } : {}),
2728
2749
  consecutiveFailures: cfg.consecutiveFailures,
@@ -2740,6 +2761,14 @@ export async function buildAutosyncStatusByChannel(dir: string): Promise<Record<
2740
2761
  interface AutosyncStatusResponse {
2741
2762
  enabled: boolean
2742
2763
  interval: AutosyncInterval
2764
+ /** 真·自动同步:本地改动即时触发(缺省 true) */
2765
+ realtime: boolean
2766
+ /** realtime 下的远端轮询间隔(ms) */
2767
+ remoteCheckMs: number
2768
+ /** 自动应用的最高风险等级(缺省 medium:plugins/mcp 也自动落盘) */
2769
+ autoApplyTier: AutosyncAutoApplyTier
2770
+ /** 双侧都改时的策略(缺省 local:保留本机并推上去) */
2771
+ conflictPolicy: AutosyncConflictPolicy
2743
2772
  lastRunAt?: string
2744
2773
  lastRunStatus?: AutosyncRunStatus
2745
2774
  lastRunMessage?: string
@@ -3598,6 +3627,10 @@ function makeRoutes(deps: RoutesDeps): { routes: WebRoute[]; scheduler: AutoSync
3598
3627
  mutationLock: host.mutationLock,
3599
3628
  isBlocked: () => host.safeModeIsBlocked?.() ?? false,
3600
3629
  phase3Recovery: host.phase3Recovery,
3630
+ // 真·自动同步:本地配置文件监听(realtime 通道)以 $DSH_HOME 为基准;
3631
+ // 监听清单由 sync/watch-scope.ts 静态派生,已排除 sessions/ 与调度器自写文件(避免自激)。
3632
+ homeDir: host.homeDir,
3633
+ ...(host.profile === undefined ? {} : { profile: host.profile }),
3601
3634
  // Phase 6:autosync 既写 sync-history.json(既有语义),也写统一迁移历史(COMPLETE 不变量)。
3602
3635
  appendHistoryFn: async (entry) => {
3603
3636
  await appendAutosyncEntry(syncDir, entry).catch(() => undefined)
@@ -3775,7 +3808,13 @@ function makeRoutes(deps: RoutesDeps): { routes: WebRoute[]; scheduler: AutoSync
3775
3808
  const port = host.mutationLock as EnvironmentLockManager | undefined
3776
3809
  if (port === undefined) return { state: 'FREE' }
3777
3810
  const insp = await port.inspectLockState()
3778
- return { state: insp.state, ...(insp.detail !== undefined ? { detail: insp.detail } : {}) }
3811
+ // ownerInstanceId 只给编排器做 journal↔锁归属匹配(判定「正在跑」vs「残留事故」),
3812
+ // 不进任何 HTTP 响应体(编排器只消费不回传)。
3813
+ return {
3814
+ state: insp.state,
3815
+ ...(insp.detail !== undefined ? { detail: insp.detail } : {}),
3816
+ ...(insp.ownerInstanceId !== undefined ? { ownerInstanceId: insp.ownerInstanceId } : {}),
3817
+ }
3779
3818
  },
3780
3819
  recoverStaleLock: async () => {
3781
3820
  const port = host.mutationLock as EnvironmentLockManager | undefined
@@ -3892,13 +3931,17 @@ function makeRoutes(deps: RoutesDeps): { routes: WebRoute[]; scheduler: AutoSync
3892
3931
  // 也不伪装成「工作副本不存在」。
3893
3932
  inspect: async (repoUrl: string): Promise<{ facts: GitWorkDirFacts } | { error: string }> => {
3894
3933
  try {
3895
- return { facts: await makeGitTransport(repoUrl).inspectWorkDir() }
3934
+ // issue #104 建议 3:`/sync/status` 要回传「领先远端提交数 / 远端快照数」,
3935
+ // 用户看不见积压,就会在「以为在备份」的状态下一直失败。detail 多出的几条命令
3936
+ // 都是本地只读(rev-list / status / ls-tree),不发网络请求。
3937
+ return { facts: await makeGitTransport(repoUrl).inspectWorkDir({ detail: true }) }
3896
3938
  } catch (err) {
3897
3939
  return { error: err instanceof Error ? err.message : String(err) }
3898
3940
  }
3899
3941
  },
3900
- // 重建:relink 非破坏;reclone 删目录重克隆(调用方必须先取得用户确认)
3901
- rebuild: async (repoUrl: string, mode: 'relink' | 'reclone'): Promise<GitWorkDirFacts> => {
3942
+ // 重建:relink 非破坏;adopt 合并远端历史(不改写本地提交,issue #104);
3943
+ // reclone 删目录重克隆(调用方必须先取得用户确认)
3944
+ rebuild: async (repoUrl: string, mode: 'relink' | 'adopt' | 'reclone'): Promise<GitWorkDirFacts> => {
3902
3945
  const transport = makeGitTransport(repoUrl)
3903
3946
  await transport.rebuildWorkDir(mode)
3904
3947
  return await transport.inspectWorkDir()
@@ -3931,7 +3974,7 @@ function makeRoutes(deps: RoutesDeps): { routes: WebRoute[]; scheduler: AutoSync
3931
3974
  /**
3932
3975
  * 路由表:本文件只保留被源码级守卫按**文件窗口**钉住的 7 条(host-entry 审计 F-04 记录的
3933
3976
  * tests/host/**、src/core/model-tools.test.ts、src/core/phase1-wiring.test.ts 的窗口断言)。
3934
- * 其余 72 条已按域拆到 src/routes/*.ts,由 buildRoutes 组装。
3977
+ * 其余 75 条已按域拆到 src/routes/*.ts,由 buildRoutes 组装。
3935
3978
  * 注册顺序不影响匹配:命名路由必须互不相同(webServer 契约)。
3936
3979
  */
3937
3980
  const routeEnv: RouteEnvInferred = makeRouteEnv()
@@ -4753,11 +4796,21 @@ export function apply(ctx: Context, config?: Config): void {
4753
4796
  warn: (line) => host.log.warn(line),
4754
4797
  })
4755
4798
  void rollbackAttribution.load()
4799
+ // 会话活跃时间窗(2026-10-18):**每个 session/event 都记一次**活跃 —— 与工具参数无关,
4800
+ // 因此 bash / 脚本 / 外部编辑器写出的路径也能按「文件 mtime 落在谁的活跃窗口内」判定归属;
4801
+ // 落在**谁都不活跃**的时刻 = 手改 / 外部工具 ⇒ 回退零写入并如实报 unclaimed(绝不静默覆盖)。
4802
+ // 同样是 best-effort:读写失败只 warn,退化成「没有时间证据」(回退行为与加它之前逐字一致)。
4803
+ const rollbackActivity = new RollbackSessionActivity({
4804
+ file: path.join(dataDir, ROLLBACK_POINTS_DIR_NAME, ROLLBACK_ACTIVITY_FILE_NAME),
4805
+ warn: (line) => host.log.warn(line),
4806
+ })
4807
+ void rollbackActivity.load()
4756
4808
  const rollbackEngine = new RollbackPointEngine({
4757
4809
  dataDir,
4758
4810
  homeDir,
4759
4811
  log: (line) => host.log.warn(line),
4760
4812
  attribution: rollbackAttribution,
4813
+ activity: rollbackActivity,
4761
4814
  fork: async ({ sessionId, boundarySeq }) => {
4762
4815
  const forkSession = forkPort.forkSession
4763
4816
  if (typeof forkSession !== 'function') {
@@ -4780,6 +4833,7 @@ export function apply(ctx: Context, config?: Config): void {
4780
4833
  engine: rollbackEngine,
4781
4834
  log: (line) => host.log.warn(line),
4782
4835
  attribution: rollbackAttribution,
4836
+ activity: rollbackActivity,
4783
4837
  })
4784
4838
  ctx.effect(() => {
4785
4839
  let chain: Promise<unknown> = Promise.resolve()
@@ -4855,6 +4909,9 @@ export function apply(ctx: Context, config?: Config): void {
4855
4909
  unlisten()
4856
4910
  detach()
4857
4911
  rollbackTracker.stop()
4912
+ // 卸载时把**未落盘**的活跃窗口刷下去(debounce 里可能还有几条):丢了它,
4913
+ // 下次启动就少一段「谁在什么时候活跃」的证据 —— 归属会因此退回「未判定」。
4914
+ void rollbackActivity.dispose()
4858
4915
  }
4859
4916
  }, 'config-manager: rollback auto points')
4860
4917
  // ------------------------------------------- 宿主斜杠命令 /rollback(t3)
@@ -98,6 +98,10 @@ test('git-file-writer: clone + 写文件 + commit + push(默认分支),凭
98
98
  const pushCall = calls[calls.length - 1]!;
99
99
  assert.deepEqual(pushCall.args.slice(pushCall.args.length - 4), ['push', '-u', 'origin', 'HEAD']);
100
100
  assert.ok(pushCall.args.some((a) => a.startsWith('credential.helper=store --file=')), 'push 必须注入 credential helper');
101
+ // issue #103:store 以 username 作查找键,只清 helper 不钉 username 会被全局 .gitconfig 顶掉
102
+ for (const call of [cloneCall, pushCall]) {
103
+ assert.ok(call.args.includes('credential.username=oauth2'), 'clone/push 必须钉住 credential.username(与 store 文件同源)');
104
+ }
101
105
  // commit message 原样传递
102
106
  const commitCall = calls.find((c) => gitCommandOf(c) === 'commit')!;
103
107
  assert.ok(commitCall.args.includes('publish: add my-config v1.0.0'));
@@ -256,7 +256,13 @@ export class GitFileWriterClient implements GitFileWriter {
256
256
  const fileArg = quoteGitValue(credFile.replace(/\\/g, '/'));
257
257
  await atomicWriteFile(credFile, `https://${this.credentialUsername}:${token}@github.com\n`, { mode: 0o600, symlink: 'reject' });
258
258
  return {
259
- extraArgs: ['-c', 'credential.helper=', '-c', `credential.helper=store --file=${fileArg}`],
259
+ // issue #103:同 git-transport.buildCredentialArgs —— store 用 username 作查找键,
260
+ // 不钉住就会被全局 `.gitconfig` 的 `credential.<url>.username` 顶掉而查不到口令。
261
+ extraArgs: [
262
+ '-c', 'credential.helper=',
263
+ '-c', `credential.helper=store --file=${fileArg}`,
264
+ '-c', `credential.username=${this.credentialUsername}`,
265
+ ],
260
266
  cleanup: async () => { await fs.rm(tmpDir, { recursive: true, force: true }); },
261
267
  };
262
268
  }
@@ -10,13 +10,13 @@
10
10
  */
11
11
  import test from 'node:test'
12
12
  import assert from 'node:assert/strict'
13
- import { mkdtempSync, rmSync } from 'node:fs'
13
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
14
14
  import { tmpdir } from 'node:os'
15
15
  import { join } from 'node:path'
16
16
 
17
17
  import {
18
- DshProfileLauncher, buildLaunchArgs, logTail, parseLaunchUrl, parseLaunches, pickFreePortCandidates,
19
- serializeLaunches,
18
+ DshProfileLauncher, buildLaunchArgs, findCliEntryForShim, logTail, parseLaunchUrl, parseLaunches,
19
+ pickFreePortCandidates, resolveDirectCliRunner, serializeLaunches,
20
20
  type DshCliCommand, type DshProfileLauncherDeps,
21
21
  } from './dsh-profile-launcher.ts'
22
22
  import { DshProfileError } from './dsh-profile-manager.ts'
@@ -376,3 +376,68 @@ test('stop:怎么都杀不掉 → stopFailed(附 pid)且记录保留(用
376
376
  assert.deepEqual(parseLaunches(h.state()).map((r) => r.name), ['work'], '杀不掉就不能清记录(否则 UI 显示「已停止」而进程还在)')
377
377
  } finally { h.cleanup() }
378
378
  })
379
+
380
+ /**
381
+ * 真机根因(Windows):启动器原先直接跑 PATH 上的 `dsh.cmd`,参数里带 `detached: true` ——
382
+ * 中间多一层 cmd.exe 后,孙进程(真正常驻的 node)的 stdout/stderr **全部丢失**,
383
+ * 日志文件恒 0 字节,于是永远读不到 `dsh web: <带 token 的 URL>`。
384
+ * 修法 = 从 .cmd 反解真正的 bin.js 直启;下面钉住反解的两条真实布局。
385
+ */
386
+ function makeShimTree(binLayout: 'sibling' | 'parent' | 'embedded-parent'): { root: string; shim: string; entry: string; cleanup: () => void } {
387
+ const root = mkdtempSync(join(tmpdir(), 'dsh-shim-'))
388
+ const prefix = binLayout === 'sibling' ? root : join(root, 'bin')
389
+ mkdirSync(prefix, { recursive: true })
390
+ const pkgRoot = join(root, 'node_modules', '@deepseek-ai', 'dsh')
391
+ mkdirSync(join(pkgRoot, 'lib'), { recursive: true })
392
+ writeFileSync(join(pkgRoot, 'package.json'), JSON.stringify({ name: '@deepseek-ai/dsh' }))
393
+ const entry = join(pkgRoot, 'lib', 'bin.js')
394
+ writeFileSync(entry, '// dsh cli\n')
395
+ const shim = join(prefix, 'dsh.cmd')
396
+ if (binLayout === 'sibling') writeFileSync(shim, '"%_prog%" "%dp0%\\node_modules\\@deepseek-ai\\dsh\\lib\\bin.js" %*\r\n')
397
+ else writeFileSync(shim, '"%_prog%" "%dp0%\\..\\node_modules\\@deepseek-ai\\dsh\\lib\\bin.js" %*\r\n')
398
+ return { root, shim, entry, cleanup: () => { rmSync(root, { recursive: true, force: true }) } }
399
+ }
400
+
401
+ test('findCliEntryForShim:npm 全局布局(node_modules 与 shim 同目录)能反解出 bin.js', () => {
402
+ const tree = makeShimTree('sibling')
403
+ try {
404
+ assert.equal(findCliEntryForShim(tree.shim), tree.entry)
405
+ } finally { tree.cleanup() }
406
+ })
407
+
408
+ test('findCliEntryForShim:shim 在 bin/ 子目录、内嵌路径带 ..\\ → 仍能反解(否则会退回 shell 包装脚本,日志恒空)', () => {
409
+ const tree = makeShimTree('embedded-parent')
410
+ try {
411
+ assert.equal(findCliEntryForShim(tree.shim), tree.entry)
412
+ } finally { tree.cleanup() }
413
+ })
414
+
415
+ test('findCliEntryForShim:找不到 dsh 入口 → null(调用方退回包装脚本,并由 warn urlNotFound 如实告知)', () => {
416
+ const root = mkdtempSync(join(tmpdir(), 'dsh-shim-none-'))
417
+ try {
418
+ const shim = join(root, 'dsh.cmd')
419
+ writeFileSync(shim, '"%_prog%" "%dp0%\\somewhere\\else.js" %*\r\n')
420
+ assert.equal(findCliEntryForShim(shim), null)
421
+ assert.equal(findCliEntryForShim(join(root, 'missing.cmd')), null, '脚本读不到也不得抛')
422
+ } finally { rmSync(root, { recursive: true, force: true }) }
423
+ })
424
+
425
+ test('resolveDirectCliRunner:宿主是 Electron 时以 ELECTRON_RUN_AS_NODE=1 直启 bin.js,普通 node 不带该变量', () => {
426
+ assert.deepEqual(resolveDirectCliRunner({ electron: '30.0.0' }), {
427
+ command: process.execPath,
428
+ env: { ELECTRON_RUN_AS_NODE: '1' },
429
+ })
430
+ assert.deepEqual(resolveDirectCliRunner({ node: '24.13.0' }), { command: process.execPath })
431
+ })
432
+
433
+ test('launch:CLI 解析成「直启 bin.js」时,spawn 不得带 shell(中间 cmd 层 + detached 会丢 stdio)', async () => {
434
+ const h = makeHarness({
435
+ cli: { command: 'C:/node/node.exe', prefixArgs: ['C:/dsh/lib/bin.js'], shell: false },
436
+ log: 'dsh web: http://127.0.0.1:3099/?token=abc',
437
+ })
438
+ try {
439
+ const result = await h.launcher.launch({ name: 'work', shape: 'web', openBrowser: false })
440
+ assert.equal(result.url, 'http://127.0.0.1:3099/?token=abc')
441
+ assert.deepEqual(result.warnings, [])
442
+ } finally { h.cleanup() }
443
+ })