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,320 @@
1
+ /**
2
+ * Claude Code **读盘层**的回归护栏(2026-10-18 审计缺陷 1/2/3 + 项目级 / CLAUDE_CONFIG_DIR)。
3
+ *
4
+ * 夹具形态一律取自本机真实布局(只复制键名与结构,**绝不含真凭据**);假 token 用 sk-ant- 形状,
5
+ * 断言它**不出现在任何返回值里**(返回的 JSON 里出现即泄露)。
6
+ *
7
+ * 三个缺陷各自钉一条:会话条数触顶报 max-sessions-reached(1)、技能目录符号链接报 symlink-skipped(2)、
8
+ * CLAUDE_CONFIG_DIR 覆盖报 source-location-overridden 且只读覆盖目录(3)。
9
+ */
10
+ import test from 'node:test';
11
+ import assert from 'node:assert/strict';
12
+ import fs from 'node:fs/promises';
13
+ import os from 'node:os';
14
+ import path from 'node:path';
15
+
16
+ import { readClaudeCode } from './read-claude-code.ts';
17
+
18
+ const NL = String.fromCharCode(10);
19
+ /** 合法的 SKILL.md(frontmatter 校验在翻译层,读盘层只看字节) */
20
+ const SKILL_MD = ['---', 'name: demo', 'description: fixture skill', '---', '', '# body', ''].join(NL);
21
+ /** 假凭据(形状像真 token,但绝不是真的;只用于「绝不泄露」断言) */
22
+ const FAKE_TOKEN = 'sk-ant-FAKE-0123456789abcdef';
23
+
24
+ async function withHome(fn: (home: string) => Promise<void>): Promise<void> {
25
+ const home = await fs.mkdtemp(path.join(os.tmpdir(), 'dcm-claude-read-'));
26
+ try {
27
+ await fn(home);
28
+ } finally {
29
+ await fs.rm(home, { recursive: true, force: true });
30
+ }
31
+ }
32
+
33
+ async function write(file: string, text: string): Promise<void> {
34
+ await fs.mkdir(path.dirname(file), { recursive: true });
35
+ await fs.writeFile(file, text);
36
+ }
37
+
38
+ /** 目录链接:win32 用 junction(不需要管理员/开发者模式),其它平台用 dir 符号链接 */
39
+ async function linkDir(target: string, linkPath: string): Promise<void> {
40
+ await fs.symlink(target, linkPath, process.platform === 'win32' ? 'junction' : 'dir');
41
+ }
42
+
43
+ test('用户级 + 项目级完整读取:结构 / 计数 / 项目级 mcpServers 都对得上', async () => {
44
+ await withHome(async (home) => {
45
+ const projA = path.join(home, 'proj-a');
46
+ const projB = path.join(home, 'proj-b');
47
+ const gone = path.join(home, 'proj-gone'); // 已删除的项目:绝不产出 finding
48
+
49
+ /* 用户级 */
50
+ await write(path.join(home, '.claude', 'settings.json'), JSON.stringify({ theme: 'dark', permissions: { allow: ['Bash'] } }));
51
+ await write(path.join(home, '.claude', 'CLAUDE.md'), '# global memory');
52
+ await write(path.join(home, '.claude', 'skills', 'user-skill', 'SKILL.md'), SKILL_MD);
53
+ await write(path.join(home, '.claude', 'commands', 'one.md'), 'ONE');
54
+ await write(path.join(home, '.claude', 'commands', 'nested', 'two.md'), 'TWO');
55
+ await write(path.join(home, '.claude', 'projects', 'slug-a', 'sess-1.jsonl'), JSON.stringify({ type: 'user' }) + NL);
56
+ await write(path.join(home, '.claude.json'), JSON.stringify({
57
+ lastVersionBase: '2.1.0',
58
+ projects: {
59
+ [projA]: { mcpServers: { 'proj-a-server': { command: 'node' } } },
60
+ [projB]: { mcpServers: { 'proj-b-server': { command: 'node' } } },
61
+ [gone]: {},
62
+ },
63
+ }));
64
+
65
+ /* 项目 A:完整项目级布局 */
66
+ await write(path.join(projA, '.claude', 'settings.json'), JSON.stringify({ permissions: { deny: ['WebFetch'] } }));
67
+ await write(path.join(projA, '.claude', 'settings.local.json'), JSON.stringify({ permissions: { allow: ['Read'] } }));
68
+ await write(path.join(projA, 'CLAUDE.md'), '# project memory');
69
+ await write(path.join(projA, 'CLAUDE.local.md'), '# local memory');
70
+ await write(path.join(projA, '.claude', 'rules', 'rule-one.md'), ['---', 'scope: src/**', '---', 'rule body'].join(NL));
71
+ await write(path.join(projA, '.claude', 'skills', 'proj-skill', 'SKILL.md'), SKILL_MD);
72
+ await write(path.join(projA, '.claude', 'agents', 'reviewer.md'), 'agent body');
73
+ await write(path.join(projA, '.claude', 'commands', 'c1.md'), 'C1');
74
+ await write(path.join(projA, '.claude', 'commands', 'deep', 'c2.md'), 'C2');
75
+ await write(path.join(projA, '.mcp.json'), JSON.stringify({ 'local-server': { command: 'npx' } }));
76
+ /* 项目 B:存在但**没有** .claude */
77
+ await fs.mkdir(projB, { recursive: true });
78
+
79
+ const read = await readClaudeCode({ homeDir: home });
80
+
81
+ assert.equal(read.found, true);
82
+ assert.equal(read.input.configDir, path.join(home, '.claude'));
83
+ assert.equal(read.input.configDirOverridden, undefined, '没有 CLAUDE_CONFIG_DIR 时不得写成「已覆盖」');
84
+ assert.deepEqual(read.input.settings, { theme: 'dark', permissions: { allow: ['Bash'] } });
85
+ assert.equal(read.input.memory, '# global memory');
86
+ assert.equal(read.input.sourceVersion, '2.1.0');
87
+ assert.equal(read.input.commandCount, 2, '全局 commands 必须**递归**计数(含 nested/)');
88
+ assert.deepEqual(read.input.skills?.map((s) => s.name), ['user-skill']);
89
+ assert.deepEqual(read.input.sessions?.map((s) => s.id), ['sess-1']);
90
+ assert.equal(
91
+ read.skipped.some((s) => s.origin === 'projects'),
92
+ false,
93
+ '没有触顶就不得报项目级码:' + JSON.stringify(read.skipped),
94
+ );
95
+
96
+ const projects = read.input.projects ?? [];
97
+ assert.deepEqual(projects.map((p) => p.root), [projA, projB], '已删除的项目必须安静跳过;其余按字典序');
98
+
99
+ const a = projects[0];
100
+ assert.ok(a !== undefined);
101
+ assert.equal(a.hasClaudeDir, true);
102
+ assert.deepEqual(a.settings, { permissions: { deny: ['WebFetch'] } });
103
+ assert.deepEqual(a.settingsLocal, { permissions: { allow: ['Read'] } });
104
+ assert.equal(a.memory, '# project memory');
105
+ assert.equal(a.memoryLocal, '# local memory');
106
+ assert.deepEqual(a.rules?.map((r) => r.name), ['rule-one.md']);
107
+ assert.deepEqual(a.skills?.map((s) => s.name), ['proj-skill']);
108
+ assert.deepEqual(a.agents, [{ name: 'reviewer', file: 'reviewer.md' }]);
109
+ assert.equal(a.commandCount, 2, '项目级 commands 必须**递归**计数');
110
+ assert.deepEqual(a.mcpJson, { 'local-server': { command: 'npx' } });
111
+ assert.deepEqual(a.projectMcp, { 'proj-a-server': { command: 'node' } }, '~/.claude.json 里该项目的 mcpServers 必须带出');
112
+ assert.equal(a.readFindings, undefined, '完整项目不得产出 finding');
113
+
114
+ const b = projects[1];
115
+ assert.deepEqual(b, { root: projB, hasClaudeDir: false }, '没有 .claude 的项目只留 root + hasClaudeDir,绝不产空 finding');
116
+ });
117
+ });
118
+
119
+ test('CLAUDE_CONFIG_DIR 覆盖:报 source-location-overridden,且**只读**覆盖后的目录(缺陷 3)', async () => {
120
+ await withHome(async (home) => {
121
+ const overrideDir = path.join(home, 'cc-config');
122
+ await write(path.join(home, '.claude', 'CLAUDE.md'), 'ORIGIN');
123
+ await write(path.join(home, '.claude', 'settings.json'), JSON.stringify({ theme: 'origin' }));
124
+ await write(path.join(overrideDir, 'CLAUDE.md'), 'OVERRIDDEN');
125
+ await write(path.join(overrideDir, 'settings.json'), JSON.stringify({ theme: 'override' }));
126
+
127
+ const read = await readClaudeCode({ homeDir: home, env: { CLAUDE_CONFIG_DIR: overrideDir } });
128
+ assert.equal(read.found, true);
129
+ assert.equal(read.input.configDir, path.resolve(overrideDir));
130
+ assert.equal(read.input.configDirOverridden, true);
131
+ assert.equal(read.input.memory, 'OVERRIDDEN', '必须读覆盖目录,绝不回落到 ~/.claude');
132
+ assert.deepEqual(read.input.settings, { theme: 'override' });
133
+ assert.ok(
134
+ read.skipped.some((s) => s.code === 'source-location-overridden' && s.origin === 'settings'),
135
+ '命中覆盖必须报码:' + JSON.stringify(read.skipped),
136
+ );
137
+ });
138
+ });
139
+
140
+ test('CLAUDE_CONFIG_DIR 空串 / 纯空白 = 未设置(不报覆盖码,照旧读 ~/.claude)', async () => {
141
+ await withHome(async (home) => {
142
+ await write(path.join(home, '.claude', 'CLAUDE.md'), 'ORIGIN');
143
+ for (const value of ['', ' ']) {
144
+ const read = await readClaudeCode({ homeDir: home, env: { CLAUDE_CONFIG_DIR: value } });
145
+ assert.equal(read.input.configDir, path.join(home, '.claude'));
146
+ assert.equal(read.input.configDirOverridden, undefined);
147
+ assert.equal(read.input.memory, 'ORIGIN');
148
+ assert.equal(read.skipped.some((s) => s.code === 'source-location-overridden'), false);
149
+ }
150
+ });
151
+ });
152
+
153
+ test('CLAUDE_CONFIG_DIR 指向「存在但读不出来」的位置:照旧报 source-unreadable', async () => {
154
+ await withHome(async (home) => {
155
+ const overrideDir = path.join(home, 'cc-broken');
156
+ await write(path.join(overrideDir, 'settings.json'), '{ this is not json');
157
+ const read = await readClaudeCode({ homeDir: home, env: { CLAUDE_CONFIG_DIR: overrideDir } });
158
+ assert.equal(read.found, true);
159
+ assert.ok(
160
+ read.skipped.some((s) => s.code === 'source-unreadable' && s.origin === '.claude/settings.json'),
161
+ '存在但读不出来必须报 source-unreadable:' + JSON.stringify(read.skipped),
162
+ );
163
+ });
164
+ });
165
+
166
+ test('会话条数触顶必须报 max-sessions-reached(缺陷 1:此前直接 break、一个字都不报)', async () => {
167
+ await withHome(async (home) => {
168
+ await write(path.join(home, '.claude', 'projects', 'slug-a', 's1.jsonl'), '{}' + NL);
169
+ await write(path.join(home, '.claude', 'projects', 'slug-a', 's2.jsonl'), '{}' + NL);
170
+ await write(path.join(home, '.claude', 'projects', 'slug-b', 's3.jsonl'), '{}' + NL);
171
+
172
+ const read = await readClaudeCode({ homeDir: home, maxSessionFiles: 2 });
173
+ assert.equal(read.input.sessions?.length, 2, '载荷里不得超过上限');
174
+ const skip = read.skipped.find((s) => s.code === 'source-unreadable' && s.detail === 'max-sessions-reached');
175
+ assert.ok(skip !== undefined, '触顶必须报码:' + JSON.stringify(read.skipped));
176
+ assert.equal(skip.origin, 'sessions');
177
+ assert.equal(skip.count, 2, 'count = 上限本身');
178
+ });
179
+ });
180
+
181
+ test('会话数恰好等于上限且没有下一个候选:不得误报 max-sessions-reached', async () => {
182
+ await withHome(async (home) => {
183
+ await write(path.join(home, '.claude', 'projects', 'slug-a', 's1.jsonl'), '{}' + NL);
184
+ await write(path.join(home, '.claude', 'projects', 'slug-a', 's2.jsonl'), '{}' + NL);
185
+ const read = await readClaudeCode({ homeDir: home, maxSessionFiles: 2 });
186
+ assert.equal(read.input.sessions?.length, 2);
187
+ assert.equal(read.skipped.some((s) => s.detail === 'max-sessions-reached'), false);
188
+ });
189
+ });
190
+
191
+ test('会话文件带出记录 sessionId 与确定性 createdAtMs(翻译层的报码与幂等回落靠它)', async () => {
192
+ await withHome(async (home) => {
193
+ const line = JSON.stringify({ type: 'user', sessionId: 'record-id-aaaa', cwd: 'D:/x', message: { role: 'user', content: 'hi' } });
194
+ await write(path.join(home, '.claude', 'projects', 'slug-a', 'file-name.jsonl'), line + NL);
195
+ const read = await readClaudeCode({ homeDir: home });
196
+ const s = read.input.sessions?.[0];
197
+ assert.ok(s !== undefined, '必须读到一条会话');
198
+ assert.equal(s.id, 'file-name', '会话 id 仍以文件名 stem 为准');
199
+ assert.equal(s.recordSessionId, 'record-id-aaaa', '记录里的 sessionId 必须原样带出');
200
+ assert.equal(typeof s.createdAtMs, 'number', 'createdAtMs 必须是数字(文件 mtime 回落)');
201
+ assert.ok(Number.isFinite(s.createdAtMs), 'createdAtMs 必须是有限数');
202
+ });
203
+ });
204
+
205
+ test('会话文件里没有 sessionId 时不伪造该字段(缺字段 = 不知道)', async () => {
206
+ await withHome(async (home) => {
207
+ await write(path.join(home, '.claude', 'projects', 'slug-a', 'no-id.jsonl'), JSON.stringify({ type: 'user', cwd: 'D:/x', message: { role: 'user', content: 'hi' } }) + NL);
208
+ const read = await readClaudeCode({ homeDir: home });
209
+ assert.equal(read.input.sessions?.[0]?.recordSessionId, undefined);
210
+ });
211
+ });
212
+
213
+ test('技能目录是符号链接:跳过并报 symlink-skipped,且绝不跟随(外部 SKILL.md 不进包)(缺陷 2)', async (t) => {
214
+ const home = await fs.mkdtemp(path.join(os.tmpdir(), 'dcm-claude-link-'));
215
+ const outside = await fs.mkdtemp(path.join(os.tmpdir(), 'dcm-claude-out-'));
216
+ t.after(async () => {
217
+ await fs.rm(home, { recursive: true, force: true });
218
+ await fs.rm(outside, { recursive: true, force: true });
219
+ });
220
+ await write(path.join(home, '.claude', 'skills', 'real', 'SKILL.md'), SKILL_MD);
221
+ await write(path.join(outside, 'SKILL.md'), SKILL_MD + NL + FAKE_TOKEN);
222
+ try {
223
+ await linkDir(outside, path.join(home, '.claude', 'skills', 'linked'));
224
+ } catch {
225
+ t.skip('本环境无法创建目录链接(权限受限),跳过符号链接用例');
226
+ return;
227
+ }
228
+
229
+ const read = await readClaudeCode({ homeDir: home });
230
+ assert.deepEqual(read.input.skills?.map((s) => s.name), ['real'], '链接目录不得被当作技能读入');
231
+ const skip = read.skipped.find((s) => s.code === 'source-unreadable' && s.detail === 'symlink-skipped');
232
+ assert.ok(skip !== undefined, '链接被跳过必须报码:' + JSON.stringify(read.skipped));
233
+ assert.equal(skip.origin, 'skills/linked', 'origin 只含包内相对标签');
234
+ assert.equal(JSON.stringify(read).includes(FAKE_TOKEN), false, '链接目标的内容绝不允许进返回值');
235
+ });
236
+
237
+ test('项目级不跟随符号链接:settings.json 指向外部文件时不读、报 symlink-skipped', async (t) => {
238
+ const home = await fs.mkdtemp(path.join(os.tmpdir(), 'dcm-claude-projlink-'));
239
+ const outside = await fs.mkdtemp(path.join(os.tmpdir(), 'dcm-claude-outs-'));
240
+ t.after(async () => {
241
+ await fs.rm(home, { recursive: true, force: true });
242
+ await fs.rm(outside, { recursive: true, force: true });
243
+ });
244
+ const proj = path.join(home, 'proj-linked');
245
+ await fs.mkdir(path.join(proj, '.claude'), { recursive: true });
246
+ const secretFile = path.join(outside, 'settings.json');
247
+ await write(secretFile, JSON.stringify({ env: { ANTHROPIC_AUTH_TOKEN: FAKE_TOKEN } }));
248
+ try {
249
+ await fs.symlink(secretFile, path.join(proj, '.claude', 'settings.json'), 'file');
250
+ } catch {
251
+ t.skip('本环境无法创建文件符号链接,跳过该用例');
252
+ return;
253
+ }
254
+
255
+ const read = await readClaudeCode({ homeDir: home, projectPaths: [proj] });
256
+ const project = read.input.projects?.[0];
257
+ assert.ok(project !== undefined);
258
+ assert.equal(project.settings, undefined, '符号链接指向的文件绝不能被读成项目 settings');
259
+ const skip = project.readFindings?.find((s) => s.detail === 'symlink-skipped');
260
+ assert.ok(skip !== undefined, '跳过必须留码:' + JSON.stringify(project.readFindings));
261
+ assert.equal(skip.origin, '.claude/settings.json');
262
+ assert.equal(JSON.stringify(read).includes(FAKE_TOKEN), false, '链接目标里的 token 绝不允许进返回值');
263
+ });
264
+
265
+ test('项目数触顶报 max-projects-reached(不静默);不存在的项目不占额度', async () => {
266
+ await withHome(async (home) => {
267
+ const roots = [path.join(home, 'p1'), path.join(home, 'p2'), path.join(home, 'p3')];
268
+ for (const root of roots) await fs.mkdir(root, { recursive: true });
269
+ const gone = path.join(home, 'p-gone');
270
+ await write(path.join(home, '.claude.json'), JSON.stringify({
271
+ projects: {
272
+ [roots[0] as string]: {},
273
+ [roots[1] as string]: {},
274
+ [roots[2] as string]: {},
275
+ [gone]: {},
276
+ },
277
+ }));
278
+
279
+ const capped = await readClaudeCode({ homeDir: home, maxProjects: 2 });
280
+ assert.equal(capped.input.projects?.length, 2);
281
+ const skip = capped.skipped.find((s) => s.code === 'source-unreadable' && s.detail === 'max-projects-reached');
282
+ assert.ok(skip !== undefined, '项目触顶必须报码:' + JSON.stringify(capped.skipped));
283
+ assert.equal(skip.origin, 'projects');
284
+ assert.equal(skip.count, 2);
285
+
286
+ const fits = await readClaudeCode({ homeDir: home, maxProjects: 3 });
287
+ assert.equal(fits.input.projects?.length, 3, '已删除的项目不占项目额度');
288
+ assert.equal(fits.skipped.some((s) => s.detail === 'max-projects-reached'), false);
289
+ });
290
+ });
291
+
292
+ test('显式 projectPaths 优先于 ~/.claude.json 的 projects 键', async () => {
293
+ await withHome(async (home) => {
294
+ const fromJson = path.join(home, 'from-json');
295
+ const explicit = path.join(home, 'explicit');
296
+ await fs.mkdir(fromJson, { recursive: true });
297
+ await fs.mkdir(explicit, { recursive: true });
298
+ await write(path.join(home, '.claude.json'), JSON.stringify({ projects: { [fromJson]: {} } }));
299
+
300
+ const read = await readClaudeCode({ homeDir: home, projectPaths: [explicit] });
301
+ assert.deepEqual(read.input.projects?.map((p) => p.root), [explicit, fromJson]);
302
+ });
303
+ });
304
+
305
+ test('假 token 绝不出现在返回值里(commands / hooks 只计数不读内容)', async () => {
306
+ await withHome(async (home) => {
307
+ await write(path.join(home, '.claude', 'settings.json'), JSON.stringify({ enabledPlugins: { 'p@m': true } }));
308
+ await write(path.join(home, '.claude', 'commands', 'secret.md'), 'curl -H "Authorization: Bearer ' + FAKE_TOKEN + '"');
309
+ const pluginDir = path.join(home, '.claude', 'plugins', 'marketplaces', 'm', 'plugins', 'p');
310
+ await write(path.join(pluginDir, '.claude-plugin', 'plugin.json'), JSON.stringify({ name: 'p' }));
311
+ await write(path.join(pluginDir, 'hooks', 'hook.js'), 'console.log(' + JSON.stringify(FAKE_TOKEN) + ')');
312
+ await write(path.join(pluginDir, 'commands', 'run.md'), 'echo ' + FAKE_TOKEN);
313
+
314
+ const read = await readClaudeCode({ homeDir: home });
315
+ assert.equal(JSON.stringify(read).includes(FAKE_TOKEN), false, '返回的整份结果里不得出现 token 形状');
316
+ assert.equal(read.input.commandCount, 1, '全局 commands 只计数');
317
+ assert.equal(read.input.plugins?.[0]?.hookFiles, 1, 'hooks 只数文件数');
318
+ assert.equal(read.input.plugins?.[0]?.commandCount, 1);
319
+ });
320
+ });