dsh-syn-config 0.8.2 → 0.8.3

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.
@@ -41,6 +41,59 @@ function section(id, title, items, extra = {}) {
41
41
  return { id, title, status, items, ...extra }
42
42
  }
43
43
 
44
+ /**
45
+ * Semantic fingerprint of a plugin manifest.
46
+ *
47
+ * `link:` specs point at the same checkout from different bases on different
48
+ * machines (`/Users/me/x` vs `../../../x`), so only the final path segment is
49
+ * meaningful. Everything else compares by value.
50
+ *
51
+ * @param {string} text
52
+ * @returns {{ deps: Record<string, string>, bundles: string[] }}
53
+ */
54
+ function manifestFingerprint(text) {
55
+ const parsed = JSON.parse(text)
56
+ const normalized = {}
57
+ for (const [name, spec] of Object.entries(parsed.dependencies ?? {})) {
58
+ const value = typeof spec === 'string' && spec.startsWith('link:')
59
+ ? `link:${spec.split(/[\\/]/u).filter((segment) => segment !== '').pop() ?? ''}`
60
+ : String(spec)
61
+ normalized[name] = value
62
+ }
63
+ const bundles = [...(parsed.dsh?.profile?.bundles ?? [])].map(String).sort()
64
+ return { deps: normalized, bundles }
65
+ }
66
+
67
+ /**
68
+ * Names of manifest entries that really differ between live and repository.
69
+ *
70
+ * @param {string} liveText
71
+ * @param {string} repoText
72
+ * @returns {string[]} human-readable differences, empty when equivalent
73
+ */
74
+ export function manifestDrift(liveText, repoText) {
75
+ let live
76
+ let repo
77
+ try {
78
+ live = manifestFingerprint(liveText)
79
+ repo = manifestFingerprint(repoText)
80
+ } catch {
81
+ return ['清单无法解析']
82
+ }
83
+ const drift = []
84
+ for (const name of new Set([...Object.keys(live.deps), ...Object.keys(repo.deps)])) {
85
+ if (live.deps[name] !== repo.deps[name]) {
86
+ drift.push(`${name} ${live.deps[name] ?? '(缺)'} ↔ ${repo.deps[name] ?? '(缺)'}`)
87
+ }
88
+ }
89
+ if (live.bundles.join(',') !== repo.bundles.join(',')) {
90
+ const onlyLive = live.bundles.filter((x) => !repo.bundles.includes(x))
91
+ const onlyRepo = repo.bundles.filter((x) => !live.bundles.includes(x))
92
+ drift.push(`bundle ${[...onlyLive.map((x) => `+${x}`), ...onlyRepo.map((x) => `-${x}`)].join(' ') || '顺序不同'}`)
93
+ }
94
+ return drift
95
+ }
96
+
44
97
  const sleep = (ms) => new Promise((done) => setTimeout(done, ms))
45
98
 
46
99
  /**
@@ -325,10 +378,10 @@ export async function runDoctor(options) {
325
378
  const liveManifestText = readTextFile(liveManifest)
326
379
  const repoManifestText = readTextFile(repoManifest)
327
380
  if (liveManifestText !== null && repoManifestText !== null) {
328
- const same = liveManifestText.replace(/\s+$/u, '') === repoManifestText.replace(/\s+$/u, '')
329
- items.push(same
330
- ? item('ok', 'package.json(插件清单)与仓库一致')
331
- : item('warn', 'package.json(插件清单)与仓库不同(装/卸了插件:sync 推送 / update 拉取)'))
381
+ const drift = manifestDrift(liveManifestText, repoManifestText)
382
+ items.push(drift.length === 0
383
+ ? item('ok', 'package.json(插件清单)与仓库一致(link: 的绝对/相对写法不参与比较)')
384
+ : item('warn', `package.json(插件清单)与仓库不同:${drift.slice(0, 5).join(',')}(装/卸了插件:sync 推送 / update 拉取)`))
332
385
  try {
333
386
  const parsed = JSON.parse(liveManifestText)
334
387
  const dependencies = Object.keys(parsed.dependencies ?? {})
package/lib/core/pack.js CHANGED
@@ -30,6 +30,7 @@ import { resolveAgentsHome, resolveDshHome } from './env.js'
30
30
  import { isFile, readTextFile, writeFileAtomic } from './files.js'
31
31
  import { mirrorDir } from './mirror.js'
32
32
  import { adoptReasoningEffort } from './patch.js'
33
+ import { loadSecrets, redact } from './secrets.js'
33
34
 
34
35
  /** Files mirrored from the profile directory; everything else stays local. */
35
36
  const PROFILE_FILES = ['package.json', 'cordis.patch.yml', 'cordis.yml', 'pnpm-lock.yaml', 'pnpm-workspace.yaml']
@@ -154,6 +155,12 @@ export function snapshot(options) {
154
155
  }
155
156
 
156
157
  // --- profile --------------------------------------------------------------
158
+ // A real value that the repository already stores as a placeholder is not a
159
+ // change to publish. Projecting redaction *before* the comparison keeps the
160
+ // plan honest — and means the plaintext never reaches the repository tree.
161
+ const secretEntries = loadSecrets(join(dshHome, 'secrets.env'))
162
+ /** Repository-form text: real secret values replaced by their placeholders. */
163
+ const forRepo = (text) => redact(text, secretEntries).text
157
164
  const profileWrites = []
158
165
  if (includeProfile && existsSync(profileDir)) {
159
166
  const previousPatch = readTextFile(join(repoProfile, 'cordis.patch.yml'))
@@ -175,9 +182,10 @@ export function snapshot(options) {
175
182
  // local machine state and must never travel.
176
183
  next = adoptReasoningEffort(next, previousPatch)
177
184
  }
185
+ const projected = forRepo(next)
178
186
  const current = readTextFile(join(repoProfile, name))
179
- if (current !== next) {
180
- profileWrites.push({ name, text: next })
187
+ if (current !== projected) {
188
+ profileWrites.push({ name, text: projected })
181
189
  changes.updated.push(`profiles/${profile}/${name}`)
182
190
  changes.files.push(`profiles/${profile}/${name}`)
183
191
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-syn-config",
3
- "version": "0.8.2",
3
+ "version": "0.8.3",
4
4
  "private": false,
5
5
  "description": "Cross-platform (macOS + Windows) DeepSeek Harness plugin that syncs DSH config — skills, profile settings, AGENTS.md — with a private dsh-config repository.",
6
6
  "type": "module",