@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.
- package/lib/adapters/credentials.d.ts +1 -1
- package/lib/adapters/index.d.ts +1 -1
- package/lib/adapters/index.js +1 -1
- package/lib/adapters/mcp.js +4 -4
- package/lib/adapters/plugins.d.ts +0 -1
- package/lib/adapters/plugins.js +35 -19
- package/lib/adapters/prompts.js +6 -6
- package/lib/adapters/test-helpers.d.ts +27 -0
- package/lib/adapters/test-helpers.js +40 -0
- package/lib/client.d.ts +1 -13
- package/lib/client.js +297 -304
- package/lib/core/analyzer.js +5 -4
- package/lib/core/backup.js +5 -3
- package/lib/core/exporter.d.ts +1 -4
- package/lib/core/exporter.js +14 -45
- package/lib/core/importer.d.ts +1 -1
- package/lib/core/index.d.ts +1 -1
- package/lib/core/messages.d.ts +1 -5
- package/lib/core/messages.js +2 -10
- package/lib/core/patch-layers.d.ts +32 -0
- package/lib/core/patch-layers.js +39 -0
- package/lib/core/rollback.js +4 -4
- package/lib/core/types.d.ts +8 -12
- package/lib/index.d.ts +23 -14
- package/lib/index.js +31 -92
- package/lib/security/index.d.ts +1 -3
- package/lib/security/index.js +1 -3
- package/lib/sync/transport.d.ts +2 -2
- package/lib/sync/transport.js +1 -1
- package/lib/ui/i18n.d.ts +0 -3
- package/lib/ui/i18n.js +0 -6
- package/lib/ui/report.js +1 -2
- package/lib/ui/test-helpers.d.ts +0 -6
- package/lib/ui/test-helpers.js +1 -5
- package/lib/ui/types.d.ts +0 -13
- package/lib/utils/env-lock.d.ts +16 -0
- package/lib/utils/env-lock.js +27 -6
- package/package.json +2 -2
- package/src/adapters/credentials.ts +1 -1
- package/src/adapters/index.ts +1 -1
- package/src/adapters/mcp.ts +4 -4
- package/src/adapters/patch-layers.test.ts +201 -0
- package/src/adapters/plugins.test.ts +13 -5
- package/src/adapters/plugins.ts +33 -17
- package/src/adapters/prompts.ts +6 -6
- package/src/adapters/test-helpers.ts +48 -0
- package/src/client/api.ts +1 -10
- package/src/client/config-manager.module.css +1 -1
- package/src/client/run-store.test.ts +1 -1
- package/src/client/sync/SyncSettingsView.tsx +5 -5
- package/src/client/sync/sync-api.ts +0 -1
- package/src/client/sync/sync-locales.ts +0 -1
- package/src/core/analyzer.ts +5 -4
- package/src/core/backup.ts +5 -3
- package/src/core/config-lifecycle.test.ts +4 -1
- package/src/core/exporter.ts +15 -47
- package/src/core/importer.ts +1 -1
- package/src/core/index.ts +1 -1
- package/src/core/messages.ts +2 -10
- package/src/core/patch-layers.ts +42 -0
- package/src/core/rollback.ts +4 -4
- package/src/core/smoke.test.ts +5 -5
- package/src/core/types.ts +8 -12
- package/src/index.facade.test.ts +25 -2
- package/src/index.ts +30 -104
- package/src/security/index.ts +1 -3
- package/src/security/security.test.ts +0 -361
- package/src/sync/transport.ts +3 -3
- package/src/ui/i18n.ts +0 -6
- package/src/ui/report.ts +1 -2
- package/src/ui/test-helpers.ts +3 -7
- package/src/ui/types.ts +0 -10
- package/src/utils/env-lock.test.ts +29 -0
- package/src/utils/env-lock.ts +27 -6
- package/lib/security/encryption.d.ts +0 -85
- package/lib/security/encryption.js +0 -279
- 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
|
-
* -
|
|
24
|
-
*
|
|
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,
|
|
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
|
|
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 {
|
|
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.
|
|
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
|
-
|
|
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 ===
|
|
538
|
-
return join(this.homeDir,
|
|
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,
|
|
541
|
-
throw new Error(this.msg('host.patchUnsupported', { user:
|
|
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
|
package/lib/security/index.d.ts
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 安全模块公共出口(m4-security):
|
|
3
|
-
* secret-scanner /
|
|
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';
|
package/lib/security/index.js
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 安全模块公共出口(m4-security):
|
|
3
|
-
* secret-scanner /
|
|
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';
|
package/lib/sync/transport.d.ts
CHANGED
|
@@ -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
|
|
32
|
+
/** 加密参数(salt/iv/authTag base64);本插件已不生成,仅供读取历史快照 */
|
|
33
33
|
info: EncryptionInfo;
|
|
34
|
-
/** base64
|
|
34
|
+
/** base64 密文(明文 = 序列化的 sections Record) */
|
|
35
35
|
data: string;
|
|
36
36
|
};
|
|
37
37
|
}
|
package/lib/sync/transport.js
CHANGED
|
@@ -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.
|
|
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('');
|
package/lib/ui/test-helpers.d.ts
CHANGED
|
@@ -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
|
}
|
package/lib/ui/test-helpers.js
CHANGED
|
@@ -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,
|
|
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 语义一致) */
|
package/lib/utils/env-lock.d.ts
CHANGED
|
@@ -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
|
*
|
package/lib/utils/env-lock.js
CHANGED
|
@@ -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
|
|
164
|
-
|
|
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
|
|
217
|
-
osIdentity = `linux:${
|
|
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.
|
|
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.
|
|
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
|
|
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';
|
package/src/adapters/index.ts
CHANGED
|
@@ -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
|
|
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';
|
package/src/adapters/mcp.ts
CHANGED
|
@@ -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 {
|
|
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(
|
|
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(
|
|
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(
|
|
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 }) };
|