@logictan/dsh-config-manager 0.1.60 → 0.1.62

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/lib/adapters/credentials.d.ts +1 -1
  2. package/lib/adapters/index.d.ts +1 -1
  3. package/lib/adapters/index.js +1 -1
  4. package/lib/adapters/mcp.js +4 -4
  5. package/lib/adapters/plugins.d.ts +0 -1
  6. package/lib/adapters/plugins.js +35 -19
  7. package/lib/adapters/prompts.js +6 -6
  8. package/lib/adapters/test-helpers.d.ts +27 -0
  9. package/lib/adapters/test-helpers.js +40 -0
  10. package/lib/client.d.ts +1 -13
  11. package/lib/client.js +297 -304
  12. package/lib/core/analyzer.js +5 -4
  13. package/lib/core/backup.js +5 -3
  14. package/lib/core/exporter.d.ts +1 -4
  15. package/lib/core/exporter.js +14 -45
  16. package/lib/core/importer.d.ts +1 -1
  17. package/lib/core/index.d.ts +1 -1
  18. package/lib/core/messages.d.ts +1 -5
  19. package/lib/core/messages.js +2 -10
  20. package/lib/core/patch-layers.d.ts +32 -0
  21. package/lib/core/patch-layers.js +39 -0
  22. package/lib/core/rollback.js +4 -4
  23. package/lib/core/types.d.ts +8 -12
  24. package/lib/index.d.ts +23 -14
  25. package/lib/index.js +31 -92
  26. package/lib/security/index.d.ts +1 -3
  27. package/lib/security/index.js +1 -3
  28. package/lib/sync/transport.d.ts +2 -2
  29. package/lib/sync/transport.js +1 -1
  30. package/lib/ui/i18n.d.ts +0 -3
  31. package/lib/ui/i18n.js +0 -6
  32. package/lib/ui/report.js +1 -2
  33. package/lib/ui/test-helpers.d.ts +0 -6
  34. package/lib/ui/test-helpers.js +1 -5
  35. package/lib/ui/types.d.ts +0 -13
  36. package/lib/utils/env-lock.d.ts +16 -0
  37. package/lib/utils/env-lock.js +27 -6
  38. package/package.json +2 -2
  39. package/src/adapters/credentials.ts +1 -1
  40. package/src/adapters/index.ts +1 -1
  41. package/src/adapters/mcp.ts +4 -4
  42. package/src/adapters/patch-layers.test.ts +201 -0
  43. package/src/adapters/plugins.test.ts +13 -5
  44. package/src/adapters/plugins.ts +33 -17
  45. package/src/adapters/prompts.ts +6 -6
  46. package/src/adapters/test-helpers.ts +48 -0
  47. package/src/client/api.ts +1 -10
  48. package/src/client/config-manager.module.css +1 -1
  49. package/src/client/run-store.test.ts +1 -1
  50. package/src/client/sync/SyncSettingsView.tsx +5 -5
  51. package/src/client/sync/sync-api.ts +0 -1
  52. package/src/client/sync/sync-locales.ts +0 -1
  53. package/src/core/analyzer.ts +5 -4
  54. package/src/core/backup.ts +5 -3
  55. package/src/core/config-lifecycle.test.ts +4 -1
  56. package/src/core/exporter.ts +15 -47
  57. package/src/core/importer.ts +1 -1
  58. package/src/core/index.ts +1 -1
  59. package/src/core/messages.ts +2 -10
  60. package/src/core/patch-layers.ts +42 -0
  61. package/src/core/rollback.ts +4 -4
  62. package/src/core/smoke.test.ts +5 -5
  63. package/src/core/types.ts +8 -12
  64. package/src/index.facade.test.ts +25 -2
  65. package/src/index.ts +30 -104
  66. package/src/security/index.ts +1 -3
  67. package/src/security/security.test.ts +0 -361
  68. package/src/sync/transport.ts +3 -3
  69. package/src/ui/i18n.ts +0 -6
  70. package/src/ui/report.ts +1 -2
  71. package/src/ui/test-helpers.ts +3 -7
  72. package/src/ui/types.ts +0 -10
  73. package/src/utils/env-lock.test.ts +29 -0
  74. package/src/utils/env-lock.ts +27 -6
  75. package/lib/security/encryption.d.ts +0 -85
  76. package/lib/security/encryption.js +0 -279
  77. package/src/security/encryption.ts +0 -335
package/lib/index.js CHANGED
@@ -20,8 +20,8 @@
20
20
  * (isLoopbackRequest); LAN-exposed deployments never serve these endpoints;
21
21
  * - uploads/exported ZIPs are staged under $DSH_HOME/dsh-config-manager/{tmp,exports}
22
22
  * and every `path`/`zipPath` reference is confined to those roots;
23
- * - the encryption password is in-memory only: used to derive the AES-256-GCM
24
- * key for secrets.enc, never written to any file, manifest, or log;
23
+ * - there is no encryption layer: every backup is plaintext, and secret values are
24
+ * stripped by the secret scanner before they reach any file, manifest, or log;
25
25
  * - the import execute endpoint refuses to run without `confirm: true`
26
26
  * (core ImportNotConfirmedError safety valve).
27
27
  *
@@ -65,11 +65,11 @@ import { registerModelTools } from './core/model-tools.js';
65
65
  import { computeConsultReport } from './core/migration-consult.js';
66
66
  import { readExportZipSource, buildLocalSnapshotSource, buildProfileSource } from './core/consult-source.js';
67
67
  import { makeMsg, msgOf, zhMsg } from './core/messages.js';
68
- import { cleanupAbortedInstall, hasDshBundlePatch, installErrorFor, installSpecFor, listInstalledPlugins, resolveProfileDir, resolveProfileNameFromArgv, readProfileManifest, runDshPlugin, validateProfileName, } from './core/plugin-cli.js';
68
+ import { cleanupAbortedInstall, hasDshBundlePatch, installErrorFor, installSpecFor, listInstalledPlugins, resolveProfileDir, resolveProfileNameFromArgv, runDshPlugin, validateProfileName, } from './core/plugin-cli.js';
69
69
  import { ImportNotConfirmedError, ImportUserSkippedError } from './core/types.js';
70
- import { createAdapters, USER_PATCH_FILE } from './adapters/index.js';
70
+ import { createAdapters } from './adapters/index.js';
71
+ import { HOME_PATCH_FILE, PROFILE_PATCH_FILE } from './core/patch-layers.js';
71
72
  import { createLocalPluginPackHook } from './core/local-plugin-host.js';
72
- import { createEncryptionProvider, decryptCredentials, decryptArchive, SecurityError, encryptArchive, isArchiveBlob, verifyEncryptedBlob } from './security/index.js';
73
73
  import { createHardenedZipParser } from './security/zip-security.js';
74
74
  import { atomicCopyFile, atomicWriteFile } from './utils/atomic-write.js';
75
75
  import { EnvironmentLockManager, runWithMutationLock, EnvironmentLockUnavailableError } from './utils/env-lock.js';
@@ -93,10 +93,9 @@ import { readUiPrefs, updateUiPrefs } from './sync/ui-prefs.js';
93
93
  import { redact } from './security/redaction.js';
94
94
  import { createConfiguredSecretScanner } from './security/secret-scanner.js';
95
95
  import { sha256Hex } from './utils/hashing.js';
96
- import { MANIFEST_FILE, parseManifest } from './schema/manifest.js';
97
96
  import { isFileSection, SECTION_IDS } from './schema/config.js';
98
97
  import { stringifyJsonSafe } from './utils/json.js';
99
- import { parseZip, zipToBuffer } from './utils/zip.js';
98
+ import { zipToBuffer } from './utils/zip.js';
100
99
  import { isSameOrChild, normalizePath } from './utils/paths.js';
101
100
  import { createLogger } from './utils/logger.js';
102
101
  /* ---------------------------------------------------------------- identity */
@@ -105,7 +104,7 @@ export const name = 'config-manager';
105
104
  /** Services required before the engine can mount (present in every profile). */
106
105
  export const inject = ['settings', 'credentials'];
107
106
  /** Plugin version, kept in sync with package.json ("version"). */
108
- const PLUGIN_VERSION = '0.1.60';
107
+ const PLUGIN_VERSION = '0.1.62';
109
108
  /** Plugin own package name — excluded from its own exported plugins list. */
110
109
  const PLUGIN_NAME = 'dsh-config-manager';
111
110
  /**
@@ -126,6 +125,8 @@ export const DEFAULT_GITHUB_CLIENT_ID = 'Ov23liq4i7n8UsylGRfb';
126
125
  /* ---------------------------------------------------------------- constants */
127
126
  /** Route family — must match the browser half's CONFIG_MANAGER_API exactly. */
128
127
  const API = {
128
+ // 设置页页脚版本行(pluginVersion / dshVersion)。只读,loopback fence。
129
+ status: '/api/dsh-config-manager/status',
129
130
  // P2-⑫:导出前只读预览(不落盘 ZIP;返回各分区 counts + 估算大小)
130
131
  // P1-⑧:快照管理(手动删除 + 置顶豁免自动清理)
131
132
  // m-backup-schedule:定时全量备份(读/存 backup-schedule.json + 立即执行一次)
@@ -520,11 +521,12 @@ class DshWorkspaceFacade {
520
521
  await registry.delete(id);
521
522
  }
522
523
  }
523
- /** Profile 目录内的 patch 文件(非 bundle 插件激活行写入处,marketplace 同款路径)。 */
524
- const PROFILE_PATCH_FILE = 'cordis.patch.yml';
525
524
  /** Patch-file facade:用户 patch 层($DSH_HOME/cordis.patch.yml)+ profile patch 层
526
- * ($DSH_HOME/profiles/<name>/cordis.patch.yml),两者都在 home 根内。 */
527
- class DshPatchFileFacade {
525
+ * ($DSH_HOME/profiles/<name>/cordis.patch.yml),两者都在 home 根内。
526
+ *
527
+ * 导出仅为让测试能用**真实门面**(而非 mock)钉住层寻址——mock 会让两个层 token 的取值
528
+ * 撞车无处暴露(DshPluginsFacade 同款做法)。 */
529
+ export class DshPatchFileFacade {
528
530
  homeDir;
529
531
  profile;
530
532
  msg;
@@ -534,11 +536,11 @@ class DshPatchFileFacade {
534
536
  this.msg = msg;
535
537
  }
536
538
  patchPath(file) {
537
- if (file === USER_PATCH_FILE)
538
- return join(this.homeDir, USER_PATCH_FILE);
539
+ if (file === HOME_PATCH_FILE)
540
+ return join(this.homeDir, HOME_PATCH_FILE);
539
541
  if (file === PROFILE_PATCH_FILE)
540
- return join(this.homeDir, 'profiles', this.profile, PROFILE_PATCH_FILE);
541
- throw new Error(this.msg('host.patchUnsupported', { user: USER_PATCH_FILE, profile: PROFILE_PATCH_FILE, file }));
542
+ return join(this.homeDir, 'profiles', this.profile, HOME_PATCH_FILE);
543
+ throw new Error(this.msg('host.patchUnsupported', { user: HOME_PATCH_FILE, profile: PROFILE_PATCH_FILE, file }));
542
544
  }
543
545
  async readPatchLines(file) {
544
546
  const p = this.patchPath(file);
@@ -781,50 +783,6 @@ async function withTimeout(promise, ms, message) {
781
783
  clearTimeout(timer);
782
784
  }
783
785
  }
784
- /** Decrypt an encrypted backup's credentials (in-memory only; undefined when not applicable). */
785
- async function tryDecryptCredentials(zipPath, password) {
786
- if (password === undefined || password === '')
787
- return undefined;
788
- const raw = await fs.readFile(zipPath);
789
- const archive = parseZip(raw);
790
- if (!archive.has(MANIFEST_FILE))
791
- return undefined;
792
- let manifest;
793
- try {
794
- manifest = parseManifest(archive.readEntryText(MANIFEST_FILE));
795
- }
796
- catch {
797
- return undefined;
798
- }
799
- if (!manifest.security.encrypted || manifest.security.encryption === null)
800
- return undefined;
801
- if (!archive.has('security/secrets.enc'))
802
- return undefined;
803
- const blob = archive.readEntry('security/secrets.enc');
804
- const plaintext = await decryptCredentials(blob, manifest.security.encryption, password);
805
- let parsed;
806
- try {
807
- parsed = yaml.load(plaintext);
808
- }
809
- catch {
810
- return undefined;
811
- }
812
- const map = new Map();
813
- if (parsed !== null && typeof parsed === 'object') {
814
- for (const [k, v] of Object.entries(parsed)) {
815
- if (typeof v === 'string' && v !== '')
816
- map.set(k, v);
817
- }
818
- }
819
- return map;
820
- }
821
- /** 解密错误 → 用户可读文本:BAD_PASSWORD 只报「密码错误」(不泄内部细节),其余原文 */
822
- function decryptErrorText(error, msg) {
823
- if (error instanceof SecurityError && error.code === 'BAD_PASSWORD') {
824
- return msg('import.encryptedPasswordWrong');
825
- }
826
- return error instanceof Error ? error.message : String(error);
827
- }
828
786
  /* -------------------------------------------------- sync 路由(m-sync-ui) */
829
787
  /** 同步路由可预期的请求级错误(status 缺省 400;引擎/传输失败走 500) */
830
788
  export class SyncRouteError extends Error {
@@ -1199,31 +1157,6 @@ function parseMeForm(raw) {
1199
1157
  export function isGitHubAuthMissing(error) {
1200
1158
  return error instanceof GitHubAuthError && (error.code === 'unauthorized' || error.code === 'no_token');
1201
1159
  }
1202
- /**
1203
- * 读取插件诊断信息(issue #28):把「插件到底读了哪个目录 / 哪个 profile / 看到什么」变成
1204
- * 用户可自查的数据——此前只存在于宿主内部,导致「装了插件却识别不到」无从定位。
1205
- * best-effort:失败不抛出(诊断位缺失不应拖垮 /status)。
1206
- */
1207
- async function readPluginDiagnostics(host) {
1208
- try {
1209
- const profileDir = resolveProfileDir(host.homeDir, host.profile ?? 'web');
1210
- const manifest = readProfileManifest(profileDir);
1211
- const installed = await host.plugins.listInstalled();
1212
- const bundles = manifest?.dsh?.profile?.bundles;
1213
- return {
1214
- homeDir: host.homeDir,
1215
- profile: host.profile ?? 'web',
1216
- profileManifestReadable: manifest !== null,
1217
- installedPluginCount: installed.length,
1218
- installedPluginNames: installed.map((p) => p.name),
1219
- bundles: Array.isArray(bundles) ? bundles : [],
1220
- };
1221
- }
1222
- catch (err) {
1223
- host.log.warn(`plugin diagnostics unavailable: ${err instanceof Error ? err.message : String(err)}`);
1224
- return {};
1225
- }
1226
- }
1227
1160
  /** Build the /api/dsh-config-manager route family. */
1228
1161
  function makeRoutes(deps) {
1229
1162
  const { host, adapters, exportsDir, tmpDir, snapshotsDir, runs, syncDir, dataDir, credentials, githubClientId, githubClientSecret, history } = deps;
@@ -1605,6 +1538,19 @@ function makeRoutes(deps) {
1605
1538
  });
1606
1539
  const routesList = [
1607
1540
  // ------------------------------------------------------------- status
1541
+ // 设置页页脚版本行:插件版本 + DSH 版本。只读、无 secret,loopback fence。
1542
+ {
1543
+ kind: 'exact',
1544
+ path: API.status,
1545
+ handler: async (req, res) => {
1546
+ if (!guard(req, res, 'GET'))
1547
+ return;
1548
+ writeJson(res, 200, {
1549
+ pluginVersion: PLUGIN_VERSION,
1550
+ dshVersion: host.dshVersion,
1551
+ });
1552
+ },
1553
+ },
1608
1554
  // ------------------------------------------------------------- export
1609
1555
  // ---------------------------------------------------- export-preview
1610
1556
  // P2-⑫:导出前只读预览(不落盘 ZIP):对选中分区逐个 adapter.export 收集 counts
@@ -1612,13 +1558,6 @@ function makeRoutes(deps) {
1612
1558
  // 零写入;loopback fence 必备。
1613
1559
  // ------------------------------------------------------------ download
1614
1560
  // -------------------------------------------------------------- upload
1615
- // ------------------------------------------------------ decrypt-archive
1616
- // 整体加密备份容器的解锁(只读,零写入到任何配置):用备份密码解密上传的加密容器,
1617
- // 得到明文 ZIP 写入受控临时目录并返回新 zipPath,供 analyze/plan/execute 引用。
1618
- // 导出时容器密码与内部 secrets.enc 密码同源(同一 password 派生两层加密),
1619
- // 因此顺带在明文 ZIP 上解出内部凭据覆盖清单(refs,非值)一并返回——
1620
- // 导入全程只需输入这一次密码,无需第二个密码校验页面。
1621
- // 密码仅内存随请求体传入,绝不落盘/落日志;解出的明文 ZIP 亦为临时文件,导入结束后清理。
1622
1561
  // ------------------------------------------------------------- analyze
1623
1562
  // ---------------------------------------------------------------- plan
1624
1563
  // ------------------------------------------------------------ progress
@@ -1,16 +1,14 @@
1
1
  /**
2
2
  * 安全模块公共出口(m4-security):
3
- * secret-scanner / encryption / integrity / zip-security / redaction。
3
+ * secret-scanner / integrity / zip-security / redaction。
4
4
  *
5
5
  * 与 core 的注入点对齐:
6
6
  * - `createSecretScanner()` → ExporterOptions.scanner(SecretScanner 契约)
7
- * - `createEncryptionProvider(pw)` → ExporterOptions.encryption(EncryptionProvider 契约)
8
7
  * - `createHardenedZipParser()` → ImporterOptions.parseZipOverride((buf, limits?) => ZipArchive)
9
8
  * - `safeExtractHardened()` → m5 导入文件类分区落盘通道
10
9
  * - `verifyChecksumsJson()` → 完整性校验通道(core analyzer 已有内置校验,本模块供独立调用)
11
10
  */
12
11
  export * from './secret-scanner.ts';
13
- export * from './encryption.ts';
14
12
  export * from './integrity.ts';
15
13
  export * from './zip-security.ts';
16
14
  export * from './redaction.ts';
@@ -1,16 +1,14 @@
1
1
  /**
2
2
  * 安全模块公共出口(m4-security):
3
- * secret-scanner / encryption / integrity / zip-security / redaction。
3
+ * secret-scanner / integrity / zip-security / redaction。
4
4
  *
5
5
  * 与 core 的注入点对齐:
6
6
  * - `createSecretScanner()` → ExporterOptions.scanner(SecretScanner 契约)
7
- * - `createEncryptionProvider(pw)` → ExporterOptions.encryption(EncryptionProvider 契约)
8
7
  * - `createHardenedZipParser()` → ImporterOptions.parseZipOverride((buf, limits?) => ZipArchive)
9
8
  * - `safeExtractHardened()` → m5 导入文件类分区落盘通道
10
9
  * - `verifyChecksumsJson()` → 完整性校验通道(core analyzer 已有内置校验,本模块供独立调用)
11
10
  */
12
11
  export * from './secret-scanner.js';
13
- export * from './encryption.js';
14
12
  export * from './integrity.js';
15
13
  export * from './zip-security.js';
16
14
  export * from './redaction.js';
@@ -29,9 +29,9 @@ export interface SyncSnapshotMeta {
29
29
  /** 加密快照的 sections 载荷:整个明文 sections 对象序列化后整体加密(AES-256-GCM)。 */
30
30
  export interface EncryptedSections {
31
31
  encrypted: {
32
- /** 加密参数(salt/iv/authTag base64;与 security/encryption.ts 的 EncryptionInfo 对齐) */
32
+ /** 加密参数(salt/iv/authTag base64);本插件已不生成,仅供读取历史快照 */
33
33
  info: EncryptionInfo;
34
- /** base64:带 DSC1 头的密文(明文 = 序列化的 sections Record) */
34
+ /** base64 密文(明文 = 序列化的 sections Record) */
35
35
  data: string;
36
36
  };
37
37
  }
@@ -21,7 +21,7 @@ export function sectionsEqual(remote, local) {
21
21
  const r = remote.sections;
22
22
  const l = local.sections;
23
23
  if (Object.keys(l).length === 0)
24
- return false; // 本地为空(加密快照)→ 无法比较
24
+ return false; // 本地无分区(空快照)→ 无法比较
25
25
  if (Object.keys(r).length !== Object.keys(l).length)
26
26
  return false;
27
27
  for (const key of Object.keys(r)) {
package/lib/ui/i18n.d.ts CHANGED
@@ -35,11 +35,9 @@ export declare const uiZh: {
35
35
  readonly 'report.security': '安全:';
36
36
  readonly 'report.apiKeysExcluded': 'API 密钥已排除:';
37
37
  readonly 'report.containsSecrets': '包含密钥:';
38
- readonly 'report.encrypted': '已加密:';
39
38
  readonly 'report.redacted': '{count} 个敏感字段已脱敏';
40
39
  readonly 'report.file': '文件:';
41
40
  readonly 'report.yes': '是';
42
- readonly 'report.yesEncrypted': '是(加密)';
43
41
  readonly 'report.no': '否';
44
42
  readonly 'report.importedRestored': '已导入/恢复';
45
43
  readonly 'report.skipped': '跳过';
@@ -151,7 +149,6 @@ export declare const uiZh: {
151
149
  readonly 'sync.pushOk': '推送成功(快照 {id})';
152
150
  readonly 'sync.pushPreviewHeadline': '将推送 {total} 个分区({changed} 个有变化)';
153
151
  readonly 'sync.pushPreviewHint': '以上为只读预览,不会写入远端。确认后点击「推送」才真正上传。';
154
- readonly 'sync.pushPreviewEncrypted': '加密快照:载荷将整体加密,各分区相对基线的变化不可比对。';
155
152
  readonly 'sync.pullFailed': '拉取失败';
156
153
  readonly 'sync.pullOk': '远端快照 {id} 差异预览:共 {count} 项变更';
157
154
  readonly 'sync.pullEmpty': '远端快照与本地一致(无变更)';
package/lib/ui/i18n.js CHANGED
@@ -38,11 +38,9 @@ export const uiZh = {
38
38
  'report.security': '安全:',
39
39
  'report.apiKeysExcluded': 'API 密钥已排除:',
40
40
  'report.containsSecrets': '包含密钥:',
41
- 'report.encrypted': '已加密:',
42
41
  'report.redacted': '{count} 个敏感字段已脱敏',
43
42
  'report.file': '文件:',
44
43
  'report.yes': '是',
45
- 'report.yesEncrypted': '是(加密)',
46
44
  'report.no': '否',
47
45
  'report.importedRestored': '已导入/恢复',
48
46
  'report.skipped': '跳过',
@@ -158,7 +156,6 @@ export const uiZh = {
158
156
  'sync.pushOk': '推送成功(快照 {id})',
159
157
  'sync.pushPreviewHeadline': '将推送 {total} 个分区({changed} 个有变化)',
160
158
  'sync.pushPreviewHint': '以上为只读预览,不会写入远端。确认后点击「推送」才真正上传。',
161
- 'sync.pushPreviewEncrypted': '加密快照:载荷将整体加密,各分区相对基线的变化不可比对。',
162
159
  'sync.pullFailed': '拉取失败',
163
160
  'sync.pullOk': '远端快照 {id} 差异预览:共 {count} 项变更',
164
161
  'sync.pullEmpty': '远端快照与本地一致(无变更)',
@@ -320,11 +317,9 @@ export const uiEn = {
320
317
  'report.security': 'Security:',
321
318
  'report.apiKeysExcluded': 'API Keys excluded:',
322
319
  'report.containsSecrets': 'Contains secrets:',
323
- 'report.encrypted': 'Encrypted:',
324
320
  'report.redacted': '{count} sensitive field(s) redacted',
325
321
  'report.file': 'File:',
326
322
  'report.yes': 'yes',
327
- 'report.yesEncrypted': 'yes (encrypted)',
328
323
  'report.no': 'no',
329
324
  'report.importedRestored': 'imported/restored',
330
325
  'report.skipped': 'skipped',
@@ -436,7 +431,6 @@ export const uiEn = {
436
431
  'sync.pushOk': 'Push succeeded (snapshot {id})',
437
432
  'sync.pushPreviewHeadline': 'Will push {total} section(s) ({changed} changed)',
438
433
  'sync.pushPreviewHint': 'Read-only preview above — nothing is written to the remote. Click "Push" to actually upload.',
439
- 'sync.pushPreviewEncrypted': 'Encrypted snapshot: the payload is encrypted as a whole; per-section changes vs the baseline cannot be compared.',
440
434
  'sync.pullFailed': 'Pull failed',
441
435
  'sync.pullOk': 'Remote snapshot {id} diff preview: {count} change(s)',
442
436
  'sync.pullEmpty': 'Remote snapshot matches local (no changes)',
package/lib/ui/report.js CHANGED
@@ -18,8 +18,7 @@ export function renderExportReport(report, t = zhUiT) {
18
18
  }
19
19
  lines.push(t('report.security'));
20
20
  lines.push(` ✓ ${t('report.apiKeysExcluded')} ${report.security.secretsExcluded ? t('report.yes') : t('report.no')}`);
21
- lines.push(` ✓ ${t('report.containsSecrets')} ${report.security.containsSecrets ? t('report.yesEncrypted') : t('report.no')}`);
22
- lines.push(` ✓ ${t('report.encrypted')} ${report.security.encrypted ? t('report.yes') : t('report.no')}`);
21
+ lines.push(` ✓ ${t('report.containsSecrets')} ${report.security.containsSecrets ? t('report.yes') : t('report.no')}`);
23
22
  if (report.security.redactedHits > 0)
24
23
  lines.push(` ⚠ ${t('report.redacted', { count: String(report.security.redactedHits) })}`);
25
24
  lines.push('');
@@ -46,7 +46,6 @@ export declare class MockImportPort implements ImportPort {
46
46
  confirm: boolean;
47
47
  secretInputs?: Record<string, string>;
48
48
  rollbackOnError: boolean;
49
- decryptPassword?: string;
50
49
  plan?: ImportPlan;
51
50
  }[];
52
51
  constructor(opts?: {
@@ -56,14 +55,9 @@ export declare class MockImportPort implements ImportPort {
56
55
  });
57
56
  analyzeImport(): Promise<ImportAnalysis>;
58
57
  createImportPlan(_zip: string, decisions: ImportDecisions): Promise<ImportPlan>;
59
- decryptArchive(zipPath: string): Promise<{
60
- zipPath: string;
61
- refs: string[];
62
- }>;
63
58
  executeImportPlan(_zip: string, plan: ImportPlan, opts: {
64
59
  confirm: boolean;
65
60
  secretInputs?: Record<string, string>;
66
61
  rollbackOnError: boolean;
67
- decryptPassword?: string;
68
62
  }): Promise<ImportResult>;
69
63
  }
@@ -20,7 +20,7 @@ export function makeExportReport(overrides = {}) {
20
20
  { section: 'plugins', counts: { plugins: 8 } },
21
21
  ],
22
22
  excluded: ['sessions', 'pluginFiles'],
23
- security: { secretsExcluded: true, containsSecrets: false, encrypted: false, redactedHits: 2 },
23
+ security: { secretsExcluded: true, containsSecrets: false, redactedHits: 2 },
24
24
  file: { name: 'dsh-config-2026-08-14.zip', sizeBytes: 20480 },
25
25
  warnings: [],
26
26
  ...overrides,
@@ -142,10 +142,6 @@ export class MockImportPort {
142
142
  this.planCalls.push(decisions);
143
143
  return this.plan;
144
144
  }
145
- async decryptArchive(zipPath) {
146
- // 测试用:把传入路径视为已解锁的明文 ZIP(不真正解密),无内部凭据
147
- return { zipPath, refs: [] };
148
- }
149
145
  async executeImportPlan(_zip, plan, opts) {
150
146
  this.executeCalls.push({ ...opts, plan });
151
147
  return this.result;
package/lib/ui/types.d.ts CHANGED
@@ -118,25 +118,12 @@ export interface RollbackView {
118
118
  export interface ImportPort {
119
119
  analyzeImport(zipPath: string): Promise<ImportAnalysis>;
120
120
  createImportPlan(zipPath: string, decisions: ImportDecisions): Promise<ImportPlan>;
121
- /**
122
- * 解锁整体加密备份(只读,零写入):用备份密码解密上传的加密容器,得到明文 ZIP
123
- * 写入受控临时目录并返回新的 zipPath,供 analyze/plan/execute 引用。
124
- * 顺带返回解密覆盖的凭据 ref 名(非值)——导出时容器密码与内部 secrets.enc
125
- * 密码同源,解锁即完成凭据解密验证,无需第二次密码校验。
126
- * 密码仅内存,绝不落盘/落日志;解密后的明文 ZIP 亦为临时文件,导入结束后清理。
127
- */
128
- decryptArchive(zipPath: string, password: string): Promise<{
129
- zipPath: string;
130
- refs: string[];
131
- }>;
132
121
  executeImportPlan(zipPath: string, plan: ImportPlan, opts: {
133
122
  /** 用户确认(安全阀,非 true 拒绝执行) */
134
123
  confirm: boolean;
135
124
  secretInputs?: Record<string, string>;
136
125
  /** 显式回滚策略:true=任一项失败整体回滚(场景 E);false=单项失败继续(§34.17) */
137
126
  rollbackOnError: boolean;
138
- /** 加密备份的解密密码(仅内存;core 拒绝加密备份无密码执行) */
139
- decryptPassword?: string;
140
127
  }): Promise<ImportResult>;
141
128
  }
142
129
  /** 选项构造辅助:从 PlanItem 提取稳定决策键(与 core analyzer.applyItemResolution 的 id 语义一致) */
@@ -222,6 +222,22 @@ export interface EnvLockManagerOptions {
222
222
  /** heartbeat 续期写失败回调(留痕;不中断 mutation) */
223
223
  onHeartbeatWriteFailure?: (err: unknown) => void;
224
224
  }
225
+ /**
226
+ * 从 /proc/<pid>/stat 文本提取进程创建身份(starttime,字段 22)。
227
+ *
228
+ * 字段布局(man 5 proc_pid_stat):1 pid, 2 comm, 3 state, …, 22 starttime, 23 vsize, 24 rss。
229
+ * comm 可能含空格与括号,故必须按**最后一个** ')' 切片;切片后 index 0 对应字段 3,
230
+ * 于是字段 N 的索引是 N-3 —— **starttime 的索引是 19**。
231
+ *
232
+ * 历史缺陷:此处曾取 index 21(= 字段 24 rss)。rss 随进程内存占用变化,
233
+ * 使同一进程的两次读取得到不同身份,被误判为「PID 复用」→ STALE_LOCK_DETECTED,
234
+ * 进而可能让 recoverStaleLock() 捕获一个**仍存活**的持有者的锁。
235
+ * 该路径仅在 Linux 生效(macOS/Windows 无 /proc),故只在 Linux CI 上暴露。
236
+ *
237
+ * @param statText /proc/<pid>/stat 全文
238
+ * @returns starttime 字符串;畸形/字段不足 → null(不抛错,也不编造身份)
239
+ */
240
+ export declare function parseLinuxProcStartTime(statText: string): string | null;
225
241
  /**
226
242
  * EnvironmentLockManager:跨进程环境锁管理器。
227
243
  *
@@ -153,16 +153,37 @@ function defaultIo() {
153
153
  } },
154
154
  };
155
155
  }
156
+ /**
157
+ * 从 /proc/<pid>/stat 文本提取进程创建身份(starttime,字段 22)。
158
+ *
159
+ * 字段布局(man 5 proc_pid_stat):1 pid, 2 comm, 3 state, …, 22 starttime, 23 vsize, 24 rss。
160
+ * comm 可能含空格与括号,故必须按**最后一个** ')' 切片;切片后 index 0 对应字段 3,
161
+ * 于是字段 N 的索引是 N-3 —— **starttime 的索引是 19**。
162
+ *
163
+ * 历史缺陷:此处曾取 index 21(= 字段 24 rss)。rss 随进程内存占用变化,
164
+ * 使同一进程的两次读取得到不同身份,被误判为「PID 复用」→ STALE_LOCK_DETECTED,
165
+ * 进而可能让 recoverStaleLock() 捕获一个**仍存活**的持有者的锁。
166
+ * 该路径仅在 Linux 生效(macOS/Windows 无 /proc),故只在 Linux CI 上暴露。
167
+ *
168
+ * @param statText /proc/<pid>/stat 全文
169
+ * @returns starttime 字符串;畸形/字段不足 → null(不抛错,也不编造身份)
170
+ */
171
+ export function parseLinuxProcStartTime(statText) {
172
+ const close = statText.lastIndexOf(')');
173
+ if (close < 0)
174
+ return null;
175
+ const fields = statText.slice(close + 1).trim().split(/\s+/);
176
+ const starttime = fields[19];
177
+ return typeof starttime === 'string' && starttime !== '' ? starttime : null;
178
+ }
156
179
  /** 默认进程探测(跨平台 best-effort;OS identity 能力由平台决定) */
157
180
  function defaultProbe() {
158
181
  const selfOsIdentity = (() => {
159
182
  try {
160
183
  if (process.platform === 'linux') {
161
- // /proc/<pid>/stat 第 22 字段 = starttime(tick 数)
162
184
  const l = fssync.readFileSync(`/proc/${process.pid}/stat`, 'utf8').toString();
163
- const afterComm = l.slice(l.lastIndexOf(')') + 1).trim().split(/\s+/);
164
- // 格式: state ppid ... starttime:comm 后第一字段是 state,starttime 是第 22 个(index 21 起)
165
- return `linux:${afterComm[21] ?? 'unknown'}`;
185
+ const starttime = parseLinuxProcStartTime(l);
186
+ return starttime === null ? null : `linux:${starttime}`;
166
187
  }
167
188
  if (process.platform === 'darwin')
168
189
  return `darwin:${process.pid}:${Date.now()}`; // 不可靠 → 保守返回占位
@@ -213,8 +234,8 @@ function defaultProbe() {
213
234
  if (process.platform === 'linux') {
214
235
  try {
215
236
  const l = fssync.readFileSync(`/proc/${pid}/stat`, 'utf8').toString();
216
- const afterComm = l.slice(l.lastIndexOf(')') + 1).trim().split(/\s+/);
217
- osIdentity = `linux:${afterComm[21] ?? 'unknown'}`;
237
+ const starttime = parseLinuxProcStartTime(l);
238
+ osIdentity = starttime === null ? null : `linux:${starttime}`;
218
239
  }
219
240
  catch {
220
241
  osIdentity = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logictan/dsh-config-manager",
3
- "version": "0.1.60",
3
+ "version": "0.1.62",
4
4
  "license": "MIT",
5
5
  "description": "Sync your DeepSeek Harness (DSH) configuration between machines over a private git or WebDAV channel: settings, plugins, MCP servers, skills, agent presets, workspaces and provider credentials. DSH 配置远程同步插件(私有通道,明文自用)。",
6
6
  "keywords": [
@@ -160,7 +160,7 @@
160
160
  "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
161
161
  "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
162
162
  "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6",
163
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
163
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.6-alpha.2",
164
164
  "@deepseek-ai/dsh-credentials": "^0.1.0-rc.6",
165
165
  "@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6",
166
166
  "@deepseek-ai/dsh-host-plugin-inventory": "^0.1.0-rc.6",
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * 安全不变量:永不导出值(hasValue 恒 false);导入生成 MissingSecret 清单,
7
7
  * 用户补录值经 ctx.secretInputs / decryptedCredentials(仅内存)→ credentials.set()。
8
- * .credentials.yaml 文件字节交由 m4 加密层处理,本 adapter 不触碰。
8
+ * .credentials.yaml 文件字节由 Exporter 的文件级 vault 处理,本 adapter 不触碰。
9
9
  */
10
10
  import type { CredentialStatus, CredentialsSection } from '../schema/types.ts';
11
11
  import { msgOf, zhMsg } from '../core/messages.ts';
@@ -79,7 +79,7 @@ export type { CredentialRefsProvider } from './credentials.ts';
79
79
  export { SettingsAdapter } from './settings.ts';
80
80
  export { UiAdapter, isUiNamespace, KNOWN_UI_NAMESPACE_PREFIXES, UI_MIGRATION_NOTES } from './ui.ts';
81
81
  export { ProvidersAdapter, DEFAULT_PROVIDER_NAMESPACES, type ProviderExportEntry, type ProviderExportSection } from './providers.ts';
82
- export { PluginsAdapter, USER_PATCH_FILE } from './plugins.ts';
82
+ export { PluginsAdapter } from './plugins.ts';
83
83
  export type { LocalPluginPackHook } from './plugins.ts';
84
84
  export { McpAdapter, extractMcpServers, buildMcpPatchLine, type McpExportEntry, type McpExportSection } from './mcp.ts';
85
85
  export { PromptsAdapter, extractPrompts, mergePromptIntoLine, buildPromptLine, type PromptExportEntry, type PromptsExportSection } from './prompts.ts';
@@ -14,7 +14,7 @@ import type {
14
14
  ApplyResult, ConfigAdapter, ExportOptions, ExportSection, HostContext,
15
15
  ImportContext, PlanItem, ValidationResult,
16
16
  } from '../core/types.ts';
17
- import { USER_PATCH_FILE } from './plugins.ts';
17
+ import { HOME_PATCH_FILE } from '../core/patch-layers.ts';
18
18
 
19
19
  /** 导出记录:McpServerEntry 之外附加来源 patch 行 id(导入写回定位用) */
20
20
  export interface McpExportEntry extends McpServerEntry {
@@ -95,7 +95,7 @@ export class McpAdapter implements ConfigAdapter<McpExportSection> {
95
95
  const warnings: string[] = [];
96
96
  let lines: { lineId: string; raw: unknown }[] = [];
97
97
  try {
98
- lines = await ctx.patchFile.readPatchLines(USER_PATCH_FILE);
98
+ lines = await ctx.patchFile.readPatchLines(HOME_PATCH_FILE);
99
99
  } catch (err) {
100
100
  warnings.push(msgOf(ctx)('adapter.patchReadFailedMCP', { reason: err instanceof Error ? err.message : String(err) }));
101
101
  }
@@ -111,7 +111,7 @@ export class McpAdapter implements ConfigAdapter<McpExportSection> {
111
111
  async analyzeImport(data: McpExportSection, ctx: ImportContext): Promise<PlanItem[]> {
112
112
  const msg = ctx.msg;
113
113
  const items: PlanItem[] = [];
114
- const targetLines = await ctx.target.patchFile.readPatchLines(USER_PATCH_FILE);
114
+ const targetLines = await ctx.target.patchFile.readPatchLines(HOME_PATCH_FILE);
115
115
  const targetServers = extractMcpServers(targetLines);
116
116
  for (const server of data.servers) {
117
117
  const id = `mcp:${server.serverName}`;
@@ -157,7 +157,7 @@ export class McpAdapter implements ConfigAdapter<McpExportSection> {
157
157
  const ref = item.target?.ref;
158
158
  if (!ref) return { ok: false, message: msg('adapter.missingTargetRef') };
159
159
  const raw = buildMcpPatchLine(ref, server);
160
- await ctx.target.patchFile.applyPatchChanges(USER_PATCH_FILE, [
160
+ await ctx.target.patchFile.applyPatchChanges(HOME_PATCH_FILE, [
161
161
  { lineId: ref, raw, action: item.kind === 'Create' ? 'insert' : 'update' },
162
162
  ]);
163
163
  return { ok: true, needsRestart: true, message: msg('adapter.mcpWritten', { serverName }) };