dsh-config-manager 0.1.0
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/README.md +216 -0
- package/README.zh-CN.md +218 -0
- package/cordis.patch.yml +45 -0
- package/dsh.bundle.patch +21 -0
- package/dsh.client +39 -0
- package/lib/adapters/agent-presets.d.ts +13 -0
- package/lib/adapters/agent-presets.js +14 -0
- package/lib/adapters/agent-presets.js.map +1 -0
- package/lib/adapters/credentials.d.ts +36 -0
- package/lib/adapters/credentials.js +121 -0
- package/lib/adapters/credentials.js.map +1 -0
- package/lib/adapters/file-collection.d.ts +14 -0
- package/lib/adapters/file-collection.js +102 -0
- package/lib/adapters/file-collection.js.map +1 -0
- package/lib/adapters/index.d.ts +38 -0
- package/lib/adapters/index.js +46 -0
- package/lib/adapters/index.js.map +1 -0
- package/lib/adapters/mcp.d.ts +27 -0
- package/lib/adapters/mcp.js +175 -0
- package/lib/adapters/mcp.js.map +1 -0
- package/lib/adapters/plugin-files.d.ts +15 -0
- package/lib/adapters/plugin-files.js +91 -0
- package/lib/adapters/plugin-files.js.map +1 -0
- package/lib/adapters/plugins.d.ts +13 -0
- package/lib/adapters/plugins.js +177 -0
- package/lib/adapters/plugins.js.map +1 -0
- package/lib/adapters/prompts.d.ts +29 -0
- package/lib/adapters/prompts.js +228 -0
- package/lib/adapters/prompts.js.map +1 -0
- package/lib/adapters/providers.d.ts +33 -0
- package/lib/adapters/providers.js +143 -0
- package/lib/adapters/providers.js.map +1 -0
- package/lib/adapters/sessions.d.ts +14 -0
- package/lib/adapters/sessions.js +15 -0
- package/lib/adapters/sessions.js.map +1 -0
- package/lib/adapters/settings.d.ts +23 -0
- package/lib/adapters/settings.js +142 -0
- package/lib/adapters/settings.js.map +1 -0
- package/lib/adapters/skills.d.ts +13 -0
- package/lib/adapters/skills.js +14 -0
- package/lib/adapters/skills.js.map +1 -0
- package/lib/adapters/test-helpers.d.ts +99 -0
- package/lib/adapters/test-helpers.js +192 -0
- package/lib/adapters/test-helpers.js.map +1 -0
- package/lib/adapters/ui.d.ts +26 -0
- package/lib/adapters/ui.js +88 -0
- package/lib/adapters/ui.js.map +1 -0
- package/lib/adapters/workspaces.d.ts +12 -0
- package/lib/adapters/workspaces.js +92 -0
- package/lib/adapters/workspaces.js.map +1 -0
- package/lib/client.d.ts +377 -0
- package/lib/client.js +2598 -0
- package/lib/client.js.map +1 -0
- package/lib/core/analyzer.d.ts +35 -0
- package/lib/core/analyzer.js +563 -0
- package/lib/core/analyzer.js.map +1 -0
- package/lib/core/backup.d.ts +29 -0
- package/lib/core/backup.js +184 -0
- package/lib/core/backup.js.map +1 -0
- package/lib/core/exporter.d.ts +37 -0
- package/lib/core/exporter.js +222 -0
- package/lib/core/exporter.js.map +1 -0
- package/lib/core/importer.d.ts +42 -0
- package/lib/core/importer.js +25 -0
- package/lib/core/importer.js.map +1 -0
- package/lib/core/index.d.ts +12 -0
- package/lib/core/index.js +11 -0
- package/lib/core/index.js.map +1 -0
- package/lib/core/rollback.d.ts +9 -0
- package/lib/core/rollback.js +136 -0
- package/lib/core/rollback.js.map +1 -0
- package/lib/core/types.d.ts +331 -0
- package/lib/core/types.js +18 -0
- package/lib/core/types.js.map +1 -0
- package/lib/core/validator.d.ts +18 -0
- package/lib/core/validator.js +77 -0
- package/lib/core/validator.js.map +1 -0
- package/lib/index.d.ts +51 -0
- package/lib/index.js +875 -0
- package/lib/index.js.map +1 -0
- package/lib/migrations/index.d.ts +29 -0
- package/lib/migrations/index.js +56 -0
- package/lib/migrations/index.js.map +1 -0
- package/lib/migrations/v1-to-v2.d.ts +9 -0
- package/lib/migrations/v1-to-v2.js +10 -0
- package/lib/migrations/v1-to-v2.js.map +1 -0
- package/lib/profiles/index.d.ts +4 -0
- package/lib/profiles/index.js +5 -0
- package/lib/profiles/index.js.map +1 -0
- package/lib/profiles/profile-manager.d.ts +102 -0
- package/lib/profiles/profile-manager.js +522 -0
- package/lib/profiles/profile-manager.js.map +1 -0
- package/lib/schema/config.d.ts +28 -0
- package/lib/schema/config.js +136 -0
- package/lib/schema/config.js.map +1 -0
- package/lib/schema/manifest.d.ts +28 -0
- package/lib/schema/manifest.js +109 -0
- package/lib/schema/manifest.js.map +1 -0
- package/lib/schema/types.d.ts +169 -0
- package/lib/schema/types.js +8 -0
- package/lib/schema/types.js.map +1 -0
- package/lib/schema/versions.d.ts +27 -0
- package/lib/schema/versions.js +50 -0
- package/lib/schema/versions.js.map +1 -0
- package/lib/security/encryption.d.ts +41 -0
- package/lib/security/encryption.js +152 -0
- package/lib/security/encryption.js.map +1 -0
- package/lib/security/index.d.ts +16 -0
- package/lib/security/index.js +17 -0
- package/lib/security/index.js.map +1 -0
- package/lib/security/integrity.d.ts +30 -0
- package/lib/security/integrity.js +78 -0
- package/lib/security/integrity.js.map +1 -0
- package/lib/security/redaction.d.ts +26 -0
- package/lib/security/redaction.js +95 -0
- package/lib/security/redaction.js.map +1 -0
- package/lib/security/secret-scanner.d.ts +48 -0
- package/lib/security/secret-scanner.js +253 -0
- package/lib/security/secret-scanner.js.map +1 -0
- package/lib/security/zip-security.d.ts +27 -0
- package/lib/security/zip-security.js +154 -0
- package/lib/security/zip-security.js.map +1 -0
- package/lib/ui/conflict-view.d.ts +28 -0
- package/lib/ui/conflict-view.js +49 -0
- package/lib/ui/conflict-view.js.map +1 -0
- package/lib/ui/errors.d.ts +17 -0
- package/lib/ui/errors.js +76 -0
- package/lib/ui/errors.js.map +1 -0
- package/lib/ui/export-flow.d.ts +62 -0
- package/lib/ui/export-flow.js +79 -0
- package/lib/ui/export-flow.js.map +1 -0
- package/lib/ui/import-wizard.d.ts +67 -0
- package/lib/ui/import-wizard.js +169 -0
- package/lib/ui/import-wizard.js.map +1 -0
- package/lib/ui/path-mapping.d.ts +38 -0
- package/lib/ui/path-mapping.js +75 -0
- package/lib/ui/path-mapping.js.map +1 -0
- package/lib/ui/progress.d.ts +27 -0
- package/lib/ui/progress.js +76 -0
- package/lib/ui/progress.js.map +1 -0
- package/lib/ui/report.d.ts +21 -0
- package/lib/ui/report.js +146 -0
- package/lib/ui/report.js.map +1 -0
- package/lib/ui/test-helpers.d.ts +62 -0
- package/lib/ui/test-helpers.js +147 -0
- package/lib/ui/test-helpers.js.map +1 -0
- package/lib/ui/types.d.ts +129 -0
- package/lib/ui/types.js +21 -0
- package/lib/ui/types.js.map +1 -0
- package/lib/utils/hashing.d.ts +17 -0
- package/lib/utils/hashing.js +41 -0
- package/lib/utils/hashing.js.map +1 -0
- package/lib/utils/json.d.ts +24 -0
- package/lib/utils/json.js +76 -0
- package/lib/utils/json.js.map +1 -0
- package/lib/utils/logger.d.ts +39 -0
- package/lib/utils/logger.js +82 -0
- package/lib/utils/logger.js.map +1 -0
- package/lib/utils/paths.d.ts +31 -0
- package/lib/utils/paths.js +132 -0
- package/lib/utils/paths.js.map +1 -0
- package/lib/utils/zip.d.ts +54 -0
- package/lib/utils/zip.js +285 -0
- package/lib/utils/zip.js.map +1 -0
- package/package.json +109 -0
- package/src/adapters/agent-presets.ts +14 -0
- package/src/adapters/credentials.test.ts +90 -0
- package/src/adapters/credentials.ts +141 -0
- package/src/adapters/file-collection.ts +110 -0
- package/src/adapters/files.test.ts +113 -0
- package/src/adapters/index.ts +69 -0
- package/src/adapters/mcp.test.ts +96 -0
- package/src/adapters/mcp.ts +181 -0
- package/src/adapters/plugin-files.ts +96 -0
- package/src/adapters/plugins.test.ts +72 -0
- package/src/adapters/plugins.ts +176 -0
- package/src/adapters/prompts.test.ts +110 -0
- package/src/adapters/prompts.ts +226 -0
- package/src/adapters/providers.test.ts +125 -0
- package/src/adapters/providers.ts +161 -0
- package/src/adapters/roundtrip.test.ts +188 -0
- package/src/adapters/sessions.ts +15 -0
- package/src/adapters/settings.test.ts +93 -0
- package/src/adapters/settings.ts +165 -0
- package/src/adapters/skills.ts +14 -0
- package/src/adapters/test-helpers.ts +207 -0
- package/src/adapters/ui.test.ts +56 -0
- package/src/adapters/ui.ts +106 -0
- package/src/adapters/workspaces.test.ts +79 -0
- package/src/adapters/workspaces.ts +95 -0
- package/src/client/CLIENT_DEPENDENCIES.md +101 -0
- package/src/client/ConfigManagerSection.tsx +60 -0
- package/src/client/api.ts +270 -0
- package/src/client/client-types.ts +27 -0
- package/src/client/common/ErrorBanner.tsx +68 -0
- package/src/client/common/ProgressBar.tsx +49 -0
- package/src/client/common/ReportView.tsx +117 -0
- package/src/client/common/ui.tsx +165 -0
- package/src/client/config-manager.module.css +759 -0
- package/src/client/css-modules.d.ts +8 -0
- package/src/client/export/ExportView.tsx +222 -0
- package/src/client/import/ConflictList.tsx +72 -0
- package/src/client/import/ImportWizardView.tsx +406 -0
- package/src/client/import/PathMappingForm.tsx +77 -0
- package/src/client/index.ts +65 -0
- package/src/client/locales.ts +182 -0
- package/src/core/analyzer.ts +631 -0
- package/src/core/backup.ts +212 -0
- package/src/core/exporter.ts +246 -0
- package/src/core/importer.ts +66 -0
- package/src/core/index.ts +39 -0
- package/src/core/rollback.ts +140 -0
- package/src/core/smoke.test.ts +717 -0
- package/src/core/types.ts +343 -0
- package/src/core/validator.ts +84 -0
- package/src/index.ts +942 -0
- package/src/migrations/index.ts +71 -0
- package/src/migrations/v1-to-v2.ts +17 -0
- package/src/profiles/index.ts +8 -0
- package/src/profiles/profile-manager.test.ts +280 -0
- package/src/profiles/profile-manager.ts +606 -0
- package/src/schema/config.ts +137 -0
- package/src/schema/manifest.ts +124 -0
- package/src/schema/types.ts +179 -0
- package/src/schema/versions.ts +55 -0
- package/src/security/encryption.ts +180 -0
- package/src/security/index.ts +16 -0
- package/src/security/integrity.ts +102 -0
- package/src/security/redaction.ts +101 -0
- package/src/security/secret-scanner.ts +275 -0
- package/src/security/security.test.ts +796 -0
- package/src/security/zip-security.ts +180 -0
- package/src/ui/conflict-view.test.ts +62 -0
- package/src/ui/conflict-view.ts +65 -0
- package/src/ui/errors.test.ts +44 -0
- package/src/ui/errors.ts +95 -0
- package/src/ui/export-flow.test.ts +75 -0
- package/src/ui/export-flow.ts +128 -0
- package/src/ui/import-wizard.test.ts +128 -0
- package/src/ui/import-wizard.ts +208 -0
- package/src/ui/path-mapping.test.ts +67 -0
- package/src/ui/path-mapping.ts +103 -0
- package/src/ui/progress.test.ts +42 -0
- package/src/ui/progress.ts +90 -0
- package/src/ui/report.test.ts +93 -0
- package/src/ui/report.ts +140 -0
- package/src/ui/test-helpers.ts +177 -0
- package/src/ui/types.ts +193 -0
- package/src/utils/hashing.ts +53 -0
- package/src/utils/json.ts +77 -0
- package/src/utils/logger.ts +114 -0
- package/src/utils/paths.ts +122 -0
- package/src/utils/zip.ts +332 -0
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration Profiles(规范 §20,Phase 6)。
|
|
3
|
+
*
|
|
4
|
+
* Profile = 用户在 DSH 中的一组配置快照(Work / Personal / Development / Minimal),
|
|
5
|
+
* 每个 Profile 含 settings / ui / providers / plugins / mcp / prompts / skills / agentPresets
|
|
6
|
+
* 等所选分区(复用 m5 ConfigAdapter 的导出数据形状)。
|
|
7
|
+
*
|
|
8
|
+
* 存储(v1):
|
|
9
|
+
* <dataDir>/profiles/<name>/profile.json
|
|
10
|
+
* - dataDir 建议 = ~/.dsh/dsh-config-manager(插件自有数据目录,参照 dsh-ssh.json 先例;
|
|
11
|
+
* 由宿主注入绝对路径,本模块用 node:fs 直接操作)
|
|
12
|
+
* - profile.json = { name, version:1, createdAt, updatedAt, sections }
|
|
13
|
+
* - 文件类分区(skills/agentPresets/sessions/pluginFiles)的 files[].data 以 base64 内嵌
|
|
14
|
+
* (Profile 单目录内自包含,便于 Duplicate / Export / Import)
|
|
15
|
+
*
|
|
16
|
+
* 安全不变量(与备份一致):
|
|
17
|
+
* - Save 走 adapter.export(describe redactSecrets)→ Profile 天然不含秘密值;
|
|
18
|
+
* - Switch 不绕过 Preview 与 Snapshot:analyzeSwitch(纯读 Preview)→ confirm → createSnapshot
|
|
19
|
+
* → 分阶段 applyItem → 失败 rollback(复用 core/backup.ts + core/rollback.ts);
|
|
20
|
+
* - Profile 名严格校验(拒绝路径穿越)。
|
|
21
|
+
*/
|
|
22
|
+
import fs from 'node:fs/promises';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import { createSnapshot } from '../core/backup.ts';
|
|
25
|
+
import { rollback } from '../core/rollback.ts';
|
|
26
|
+
import { ImportNotConfirmedError } from '../core/types.ts';
|
|
27
|
+
import { isFileSection } from '../schema/config.ts';
|
|
28
|
+
import { stringifyJsonSafe, parseJsonSafe } from '../utils/json.ts';
|
|
29
|
+
import type {
|
|
30
|
+
ApplyResult, ConfigAdapter, ExecutedItem, GlobalConflictStrategy, HostContext,
|
|
31
|
+
ImportContext, ImportPlan, ItemResolution, PlanItem, RollbackReport,
|
|
32
|
+
SnapshotStore,
|
|
33
|
+
} from '../core/types.ts';
|
|
34
|
+
import type { FilesSection, Manifest, SectionId } from '../schema/types.ts';
|
|
35
|
+
|
|
36
|
+
/* ---------------- 类型 ---------------- */
|
|
37
|
+
|
|
38
|
+
export interface ProfileMeta {
|
|
39
|
+
name: string;
|
|
40
|
+
createdAt: string;
|
|
41
|
+
updatedAt: string;
|
|
42
|
+
sections: SectionId[];
|
|
43
|
+
fileCount: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** profile.json 的磁盘形状(文件类分区 data 为 base64) */
|
|
47
|
+
export interface StoredProfile {
|
|
48
|
+
name: string;
|
|
49
|
+
version: 1;
|
|
50
|
+
createdAt: string;
|
|
51
|
+
updatedAt: string;
|
|
52
|
+
sections: Partial<Record<SectionId, unknown>>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface SwitchPreview {
|
|
56
|
+
items: PlanItem[];
|
|
57
|
+
missingSecrets: { ref: string; required: boolean }[];
|
|
58
|
+
needsRestart: boolean;
|
|
59
|
+
sectionsInProfile: SectionId[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ProfileSwitchResult {
|
|
63
|
+
ok: boolean;
|
|
64
|
+
executed: ExecutedItem[];
|
|
65
|
+
needsRestart: boolean;
|
|
66
|
+
missingSecrets: string[];
|
|
67
|
+
warnings: string[];
|
|
68
|
+
rollback: RollbackReport | null;
|
|
69
|
+
snapshotId: string | null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ProfileManagerOptions {
|
|
73
|
+
/** Profile 根目录(建议 ~/.dsh/dsh-config-manager,绝对路径) */
|
|
74
|
+
dataDir: string;
|
|
75
|
+
ctx: HostContext;
|
|
76
|
+
adapters: ConfigAdapter[];
|
|
77
|
+
snapshotStore: SnapshotStore;
|
|
78
|
+
/** Save/切换涉及的默认分区(缺省 = adapter.defaultIncluded 的分区) */
|
|
79
|
+
includeSections?: SectionId[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 执行阶段顺序(与 core/analyzer.ts APPLY_ORDER 对齐:副作用大的 patch/安装最后) */
|
|
83
|
+
export const APPLY_ORDER: readonly SectionId[] = [
|
|
84
|
+
'settings', 'ui', 'providers', 'prompts', 'skills', 'agentPresets',
|
|
85
|
+
'workspaces', 'pluginFiles', 'mcp', 'plugins', 'credentialsStatus',
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
/** Profile 名校验:拒绝路径穿越与非法字符 */
|
|
89
|
+
export function isValidProfileName(name: string): boolean {
|
|
90
|
+
if (name === '' || name.length > 64) return false;
|
|
91
|
+
if (name === '.' || name === '..') return false;
|
|
92
|
+
if (name.includes('/') || name.includes('\\') || name.includes('\0')) return false;
|
|
93
|
+
if (name.includes('..')) return false;
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* ---------------- 序列化(文件类分区 base64 内嵌) ---------------- */
|
|
98
|
+
|
|
99
|
+
interface StoredFileEntry { relativePath: string; data: string; contentHash: string }
|
|
100
|
+
|
|
101
|
+
/** Profile 落盘编码:文件类分区 files[].data(Uint8Array) → base64;JSON 分区原样(已可序列化) */
|
|
102
|
+
export function encodeSections(sections: ReadonlyMap<SectionId, unknown>): Partial<Record<SectionId, unknown>> {
|
|
103
|
+
const out: Partial<Record<SectionId, unknown>> = {};
|
|
104
|
+
for (const [sectionId, data] of sections) {
|
|
105
|
+
if (isFileSection(sectionId)) {
|
|
106
|
+
const files = (data as FilesSection).files ?? [];
|
|
107
|
+
out[sectionId] = {
|
|
108
|
+
version: 1,
|
|
109
|
+
files: files.map((f) => ({
|
|
110
|
+
relativePath: f.relativePath,
|
|
111
|
+
data: Buffer.from(f.data).toString('base64'),
|
|
112
|
+
contentHash: f.contentHash,
|
|
113
|
+
})),
|
|
114
|
+
};
|
|
115
|
+
} else {
|
|
116
|
+
out[sectionId] = data;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Profile 读入解码:base64 → Uint8Array */
|
|
123
|
+
export function decodeSections(encoded: Partial<Record<SectionId, unknown>>): Map<SectionId, unknown> {
|
|
124
|
+
const sections = new Map<SectionId, unknown>();
|
|
125
|
+
for (const [sectionId, data] of Object.entries(encoded) as [SectionId, unknown][]) {
|
|
126
|
+
if (isFileSection(sectionId) && data !== null && typeof data === 'object') {
|
|
127
|
+
const files = ((data as { files?: StoredFileEntry[] }).files ?? []).map((f) => ({
|
|
128
|
+
relativePath: f.relativePath,
|
|
129
|
+
data: Uint8Array.from(Buffer.from(f.data, 'base64')),
|
|
130
|
+
contentHash: f.contentHash,
|
|
131
|
+
}));
|
|
132
|
+
sections.set(sectionId, { version: 1, files });
|
|
133
|
+
} else {
|
|
134
|
+
sections.set(sectionId, data);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return sections;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* ---------------- ProfileManager ---------------- */
|
|
141
|
+
|
|
142
|
+
export class ProfileManager {
|
|
143
|
+
private readonly dataDir: string;
|
|
144
|
+
private readonly ctx: HostContext;
|
|
145
|
+
private readonly adapters: ConfigAdapter[];
|
|
146
|
+
private readonly snapshotStore: SnapshotStore;
|
|
147
|
+
private readonly includeSections: SectionId[];
|
|
148
|
+
private readonly profilesDir: string;
|
|
149
|
+
|
|
150
|
+
constructor(options: ProfileManagerOptions) {
|
|
151
|
+
this.dataDir = options.dataDir;
|
|
152
|
+
this.ctx = options.ctx;
|
|
153
|
+
this.adapters = options.adapters;
|
|
154
|
+
this.snapshotStore = options.snapshotStore;
|
|
155
|
+
this.includeSections = options.includeSections ??
|
|
156
|
+
this.adapters.filter((a) => a.defaultIncluded).map((a) => a.id);
|
|
157
|
+
this.profilesDir = path.join(this.dataDir, 'profiles');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* ---------------- 基础 ---------------- */
|
|
161
|
+
|
|
162
|
+
async list(): Promise<ProfileMeta[]> {
|
|
163
|
+
let entries: string[] = [];
|
|
164
|
+
try {
|
|
165
|
+
entries = await fs.readdir(this.profilesDir, { withFileTypes: true }).then((es) =>
|
|
166
|
+
es.filter((e) => e.isDirectory()).map((e) => e.name).sort());
|
|
167
|
+
} catch {
|
|
168
|
+
return []; // 目录不存在视为空
|
|
169
|
+
}
|
|
170
|
+
const metas: ProfileMeta[] = [];
|
|
171
|
+
for (const name of entries) {
|
|
172
|
+
try {
|
|
173
|
+
metas.push(await this.meta(name));
|
|
174
|
+
} catch {
|
|
175
|
+
// 损坏的 profile 目录跳过(如实不报)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return metas;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private profileDir(name: string): string {
|
|
182
|
+
return path.join(this.profilesDir, name);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private profileFile(name: string): string {
|
|
186
|
+
return path.join(this.profileDir(name), 'profile.json');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private async meta(name: string): Promise<ProfileMeta> {
|
|
190
|
+
const stored = await this.readStored(name);
|
|
191
|
+
const sections = Object.keys(stored.sections) as SectionId[];
|
|
192
|
+
return {
|
|
193
|
+
name: stored.name,
|
|
194
|
+
createdAt: stored.createdAt,
|
|
195
|
+
updatedAt: stored.updatedAt,
|
|
196
|
+
sections,
|
|
197
|
+
fileCount: sections.length,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private async readStored(name: string): Promise<StoredProfile> {
|
|
202
|
+
if (!isValidProfileName(name)) throw new Error(`非法 Profile 名: "${name}"`);
|
|
203
|
+
let raw: string;
|
|
204
|
+
try {
|
|
205
|
+
raw = await fs.readFile(this.profileFile(name), 'utf8');
|
|
206
|
+
} catch (err) {
|
|
207
|
+
throw new Error(`Profile "${name}" 不存在或不可读: ${err instanceof Error ? err.message : String(err)}`);
|
|
208
|
+
}
|
|
209
|
+
const parsed = parseJsonSafe(raw) as StoredProfile;
|
|
210
|
+
if (parsed === null || typeof parsed !== 'object' || parsed.name !== name || parsed.version !== 1) {
|
|
211
|
+
throw new Error(`Profile "${name}" 数据无效(name/version 不匹配)`);
|
|
212
|
+
}
|
|
213
|
+
if (parsed.sections === null || typeof parsed.sections !== 'object') {
|
|
214
|
+
throw new Error(`Profile "${name}" 缺少 sections`);
|
|
215
|
+
}
|
|
216
|
+
return parsed;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private async writeStored(stored: StoredProfile): Promise<void> {
|
|
220
|
+
await fs.mkdir(this.profileDir(stored.name), { recursive: true });
|
|
221
|
+
await fs.writeFile(this.profileFile(stored.name), stringifyJsonSafe(stored, { space: 2 }), 'utf8');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/* ---------------- Save / Duplicate / Rename / Delete ---------------- */
|
|
225
|
+
|
|
226
|
+
/** 保存当前 DSH 配置为 Profile(复用 adapter.export,天然不含秘密值) */
|
|
227
|
+
async saveCurrent(name: string, opts: { sections?: SectionId[] } = {}): Promise<ProfileMeta> {
|
|
228
|
+
if (!isValidProfileName(name)) throw new Error(`非法 Profile 名: "${name}"`);
|
|
229
|
+
const selected = opts.sections ?? this.includeSections;
|
|
230
|
+
const sections = new Map<SectionId, unknown>();
|
|
231
|
+
for (const adapter of this.adapters) {
|
|
232
|
+
if (!selected.includes(adapter.id)) continue;
|
|
233
|
+
try {
|
|
234
|
+
const section = await adapter.export(this.ctx, { includeSecrets: false, only: [adapter.id] });
|
|
235
|
+
const isEmpty = section.counts && Object.values(section.counts).every((n) => n === 0);
|
|
236
|
+
if (!isEmpty) sections.set(adapter.id, section.data);
|
|
237
|
+
} catch (err) {
|
|
238
|
+
this.ctx.log.warn(`保存 Profile "${name}" 时分区 ${adapter.id} 导出失败,已跳过: ${err instanceof Error ? err.message : String(err)}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (sections.size === 0) {
|
|
242
|
+
throw new Error(`Profile "${name}" 没有任何可保存的配置分区`);
|
|
243
|
+
}
|
|
244
|
+
const now = new Date().toISOString();
|
|
245
|
+
const stored: StoredProfile = {
|
|
246
|
+
name,
|
|
247
|
+
version: 1,
|
|
248
|
+
createdAt: now,
|
|
249
|
+
updatedAt: now,
|
|
250
|
+
sections: encodeSections(sections),
|
|
251
|
+
};
|
|
252
|
+
await this.writeStored(stored);
|
|
253
|
+
return this.meta(name);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** 复制 Profile(含全部分区数据) */
|
|
257
|
+
async duplicate(name: string, newName: string): Promise<ProfileMeta> {
|
|
258
|
+
if (!isValidProfileName(newName)) throw new Error(`非法新 Profile 名: "${newName}"`);
|
|
259
|
+
const stored = await this.readStored(name);
|
|
260
|
+
const now = new Date().toISOString();
|
|
261
|
+
const copy: StoredProfile = {
|
|
262
|
+
...structuredClone(stored),
|
|
263
|
+
name: newName,
|
|
264
|
+
createdAt: now,
|
|
265
|
+
updatedAt: now,
|
|
266
|
+
};
|
|
267
|
+
await this.writeStored(copy);
|
|
268
|
+
return this.meta(newName);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** 重命名 Profile(目录级移动) */
|
|
272
|
+
async rename(oldName: string, newName: string): Promise<ProfileMeta> {
|
|
273
|
+
if (!isValidProfileName(newName)) throw new Error(`非法新 Profile 名: "${newName}"`);
|
|
274
|
+
const stored = await this.readStored(oldName); // 存在性检查
|
|
275
|
+
if (oldName === newName) return this.meta(oldName);
|
|
276
|
+
if (await this.exists(newName)) throw new Error(`Profile "${newName}" 已存在`);
|
|
277
|
+
stored.name = newName;
|
|
278
|
+
stored.updatedAt = new Date().toISOString();
|
|
279
|
+
await fs.mkdir(this.profileDir(newName), { recursive: true });
|
|
280
|
+
await fs.writeFile(this.profileFile(newName), stringifyJsonSafe(stored, { space: 2 }), 'utf8');
|
|
281
|
+
await fs.rm(this.profileDir(oldName), { recursive: true, force: true });
|
|
282
|
+
return this.meta(newName);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** 删除 Profile */
|
|
286
|
+
async delete(name: string): Promise<void> {
|
|
287
|
+
if (!isValidProfileName(name)) throw new Error(`非法 Profile 名: "${name}"`);
|
|
288
|
+
await this.readStored(name); // 存在性检查
|
|
289
|
+
await fs.rm(this.profileDir(name), { recursive: true, force: true });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private async exists(name: string): Promise<boolean> {
|
|
293
|
+
try {
|
|
294
|
+
await fs.access(this.profileFile(name));
|
|
295
|
+
return true;
|
|
296
|
+
} catch {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/* ---------------- Switch(Preview + Snapshot + Rollback 安全流程) ---------------- */
|
|
302
|
+
|
|
303
|
+
/** Preview(纯读,零写入):分析切换到该 Profile 会产生的计划项 */
|
|
304
|
+
async analyzeSwitch(name: string): Promise<SwitchPreview> {
|
|
305
|
+
const stored = await this.readStored(name);
|
|
306
|
+
const sections = decodeSections(stored.sections);
|
|
307
|
+
const { items } = await this.analyzeSections(sections);
|
|
308
|
+
const missingSecrets = items
|
|
309
|
+
.filter((i) => i.kind === 'MissingSecret')
|
|
310
|
+
.map((i) => ({ ref: i.id.replace(/^secret:/, ''), required: true }));
|
|
311
|
+
const needsRestart = items.some(
|
|
312
|
+
(i) => i.kind === 'Install' || (i.adapter === 'mcp' && i.kind !== 'Skip' && i.kind !== 'Warning'),
|
|
313
|
+
);
|
|
314
|
+
return {
|
|
315
|
+
items,
|
|
316
|
+
missingSecrets,
|
|
317
|
+
needsRestart,
|
|
318
|
+
sectionsInProfile: [...sections.keys()],
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** 执行切换:confirm → Snapshot → 分阶段 apply → 失败 rollback(与导入同一安全语义) */
|
|
323
|
+
async executeSwitch(
|
|
324
|
+
name: string,
|
|
325
|
+
opts: {
|
|
326
|
+
confirm?: boolean;
|
|
327
|
+
strategy?: GlobalConflictStrategy;
|
|
328
|
+
resolutions?: Record<string, ItemResolution>;
|
|
329
|
+
secretInputs?: Record<string, string>;
|
|
330
|
+
decryptedCredentials?: Map<string, string>;
|
|
331
|
+
rollbackOnError?: boolean;
|
|
332
|
+
} = {},
|
|
333
|
+
): Promise<ProfileSwitchResult> {
|
|
334
|
+
// 安全阀:不确认绝不动数据(对齐 ImportNotConfirmedError)
|
|
335
|
+
if (opts.confirm !== true) throw new ImportNotConfirmedError();
|
|
336
|
+
|
|
337
|
+
const stored = await this.readStored(name);
|
|
338
|
+
const sections = decodeSections(stored.sections);
|
|
339
|
+
const { items: rawItems } = await this.analyzeSections(sections);
|
|
340
|
+
const strategy = opts.strategy ?? 'merge';
|
|
341
|
+
const items = rawItems.map((item) => applyResolution(item, strategy, opts.resolutions ?? {}));
|
|
342
|
+
|
|
343
|
+
const missingSecrets = items
|
|
344
|
+
.filter((i) => i.kind === 'MissingSecret')
|
|
345
|
+
.map((i) => ({ ref: i.id.replace(/^secret:/, ''), required: true }));
|
|
346
|
+
const needsRestart = items.some(
|
|
347
|
+
(i) => i.kind === 'Install' || (i.adapter === 'mcp' && i.kind !== 'Skip' && i.kind !== 'Warning'),
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
const plan: ImportPlan = {
|
|
351
|
+
items,
|
|
352
|
+
globalStrategy: strategy,
|
|
353
|
+
pathMappings: [],
|
|
354
|
+
missingSecrets,
|
|
355
|
+
needsRestart,
|
|
356
|
+
estimatedActions: {} as ImportPlan['estimatedActions'],
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
// Snapshot(强制:切换前备份将被修改的目标,复用 core/backup.ts)
|
|
360
|
+
const snapshot = await createSnapshot({
|
|
361
|
+
ctx: this.ctx,
|
|
362
|
+
plan,
|
|
363
|
+
sourceZip: `profile:${name}`,
|
|
364
|
+
store: this.snapshotStore,
|
|
365
|
+
adapters: this.adapters,
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const importCtx: ImportContext = {
|
|
369
|
+
manifest: buildProfileManifest(this.ctx),
|
|
370
|
+
targetPlatform: this.ctx.platform,
|
|
371
|
+
target: this.ctx,
|
|
372
|
+
sections,
|
|
373
|
+
pathMappings: [],
|
|
374
|
+
resolutions: opts.resolutions ?? {},
|
|
375
|
+
secretInputs: opts.secretInputs ?? {},
|
|
376
|
+
decryptedCredentials: opts.decryptedCredentials,
|
|
377
|
+
log: this.ctx.log,
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const executed: ExecutedItem[] = [];
|
|
381
|
+
const warnings: string[] = [];
|
|
382
|
+
let anyFailed = false;
|
|
383
|
+
let finalNeedsRestart = needsRestart;
|
|
384
|
+
|
|
385
|
+
const byAdapter = new Map<SectionId, PlanItem[]>();
|
|
386
|
+
for (const item of items) {
|
|
387
|
+
const list = byAdapter.get(item.adapter) ?? [];
|
|
388
|
+
list.push(item);
|
|
389
|
+
byAdapter.set(item.adapter, list);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
for (const adapterId of APPLY_ORDER) {
|
|
393
|
+
const adapter = this.adapters.find((a) => a.id === adapterId);
|
|
394
|
+
if (!adapter) continue;
|
|
395
|
+
for (const item of byAdapter.get(adapterId) ?? []) {
|
|
396
|
+
const outcome = await this.applyOne(adapter, item, importCtx);
|
|
397
|
+
executed.push(outcome.executed);
|
|
398
|
+
if (outcome.executed.status === 'failed') anyFailed = true;
|
|
399
|
+
if (outcome.needsRestart) finalNeedsRestart = true;
|
|
400
|
+
if (outcome.warning) warnings.push(outcome.warning);
|
|
401
|
+
if (opts.rollbackOnError && outcome.executed.status === 'failed') break;
|
|
402
|
+
}
|
|
403
|
+
if (opts.rollbackOnError && anyFailed) break;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// 失败整体回滚(复用 core/rollback.ts,逆序补偿 + 诚实报告)
|
|
407
|
+
if (opts.rollbackOnError && anyFailed) {
|
|
408
|
+
const rollbackReport = await rollback({
|
|
409
|
+
ctx: this.ctx,
|
|
410
|
+
snapshot,
|
|
411
|
+
store: this.snapshotStore,
|
|
412
|
+
adapters: this.adapters,
|
|
413
|
+
});
|
|
414
|
+
this.ctx.log.warn(`Profile "${name}" 切换失败,已回滚(${rollbackReport.full ? '完整' : '部分'})`, {
|
|
415
|
+
failed: executed.filter((e) => e.status === 'failed').map((e) => e.itemId),
|
|
416
|
+
});
|
|
417
|
+
return {
|
|
418
|
+
ok: false,
|
|
419
|
+
executed,
|
|
420
|
+
needsRestart: false,
|
|
421
|
+
missingSecrets: [],
|
|
422
|
+
warnings,
|
|
423
|
+
rollback: rollbackReport,
|
|
424
|
+
snapshotId: snapshot.id,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const remainingSecrets = missingSecrets
|
|
429
|
+
.filter((s) => !importCtx.decryptedCredentials?.has(s.ref) && !importCtx.secretInputs[s.ref])
|
|
430
|
+
.map((s) => s.ref);
|
|
431
|
+
|
|
432
|
+
return {
|
|
433
|
+
ok: true,
|
|
434
|
+
executed,
|
|
435
|
+
needsRestart: finalNeedsRestart,
|
|
436
|
+
missingSecrets: remainingSecrets,
|
|
437
|
+
warnings,
|
|
438
|
+
rollback: null,
|
|
439
|
+
snapshotId: snapshot.id,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** 逐分区 validate + analyzeImport(纯计算;MissingSecret 兜底与 analyzer.ensureMissingSecrets 等价) */
|
|
444
|
+
private async analyzeSections(
|
|
445
|
+
sections: Map<SectionId, unknown>,
|
|
446
|
+
): Promise<{ items: PlanItem[]; issues: string[] }> {
|
|
447
|
+
const importCtx: ImportContext = {
|
|
448
|
+
manifest: buildProfileManifest(this.ctx),
|
|
449
|
+
targetPlatform: this.ctx.platform,
|
|
450
|
+
target: this.ctx,
|
|
451
|
+
sections,
|
|
452
|
+
pathMappings: [],
|
|
453
|
+
resolutions: {},
|
|
454
|
+
secretInputs: {},
|
|
455
|
+
log: this.ctx.log,
|
|
456
|
+
};
|
|
457
|
+
const items: PlanItem[] = [];
|
|
458
|
+
const issues: string[] = [];
|
|
459
|
+
for (const adapter of this.adapters) {
|
|
460
|
+
const data = sections.get(adapter.id);
|
|
461
|
+
if (data === undefined) continue;
|
|
462
|
+
try {
|
|
463
|
+
const v = await adapter.validate(data);
|
|
464
|
+
for (const issue of v.issues) {
|
|
465
|
+
if (issue.severity === 'error') issues.push(`${adapter.id}: ${issue.message}`);
|
|
466
|
+
}
|
|
467
|
+
if (!v.valid) continue;
|
|
468
|
+
items.push(...await adapter.analyzeImport(data, importCtx));
|
|
469
|
+
} catch (err) {
|
|
470
|
+
issues.push(`${adapter.id}: 分析失败 ${err instanceof Error ? err.message : String(err)}`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
// MissingSecret 兜底:credentialsStatus 分区里已配置凭据若没有对应计划项,补占位
|
|
474
|
+
const creds = sections.get('credentialsStatus') as { credentials?: { ref?: string; configured?: boolean }[] } | undefined;
|
|
475
|
+
if (creds?.credentials) {
|
|
476
|
+
const existing = new Set(items.filter((i) => i.kind === 'MissingSecret').map((i) => i.id));
|
|
477
|
+
for (const c of creds.credentials) {
|
|
478
|
+
if (!c.ref || c.configured !== true) continue;
|
|
479
|
+
const id = `secret:${c.ref}`;
|
|
480
|
+
if (existing.has(id)) continue;
|
|
481
|
+
items.push({
|
|
482
|
+
id, kind: 'MissingSecret', adapter: 'credentialsStatus',
|
|
483
|
+
description: `凭据 ${c.ref} 需要补录`, severity: 'warning',
|
|
484
|
+
target: { adapter: 'credentialsStatus', ref: c.ref },
|
|
485
|
+
});
|
|
486
|
+
existing.add(id);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return { items, issues };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** 单计划项执行(语义对齐 core/analyzer.ts applyOne) */
|
|
493
|
+
private async applyOne(
|
|
494
|
+
adapter: ConfigAdapter,
|
|
495
|
+
item: PlanItem,
|
|
496
|
+
ctx: ImportContext,
|
|
497
|
+
): Promise<{ executed: ExecutedItem; needsRestart: boolean; warning?: string }> {
|
|
498
|
+
if (item.kind === 'Skip' || (item.kind === 'Conflict' && item.conflict?.resolution !== 'useImported')) {
|
|
499
|
+
return { executed: { itemId: item.id, status: 'skipped' }, needsRestart: false };
|
|
500
|
+
}
|
|
501
|
+
if (item.kind === 'Error') {
|
|
502
|
+
return { executed: { itemId: item.id, status: 'failed', message: item.detail ?? item.description }, needsRestart: false };
|
|
503
|
+
}
|
|
504
|
+
if (item.kind === 'MissingSecret') {
|
|
505
|
+
const ref = item.id.replace(/^secret:/, '');
|
|
506
|
+
const value = ctx.decryptedCredentials?.get(ref) ?? ctx.secretInputs[ref];
|
|
507
|
+
if (value === undefined || value === '') {
|
|
508
|
+
return { executed: { itemId: item.id, status: 'skipped', message: '凭据未提供,需补录' }, needsRestart: false };
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
try {
|
|
512
|
+
const result: ApplyResult = await adapter.applyItem(item, ctx);
|
|
513
|
+
return {
|
|
514
|
+
executed: { itemId: item.id, status: result.ok ? 'ok' : 'failed', message: result.message },
|
|
515
|
+
needsRestart: result.needsRestart === true,
|
|
516
|
+
};
|
|
517
|
+
} catch (err) {
|
|
518
|
+
this.ctx.log.error(`切换 Profile 应用计划项失败 ${item.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
519
|
+
return {
|
|
520
|
+
executed: { itemId: item.id, status: 'failed', message: err instanceof Error ? err.message : String(err) },
|
|
521
|
+
needsRestart: false,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/* ---------------- Export / Import Profile ---------------- */
|
|
527
|
+
|
|
528
|
+
/** 导出 Profile 为单 JSON 文件(dsh-profile-<name>.json,自包含可分发;文件类数据 base64 内嵌) */
|
|
529
|
+
async exportProfile(name: string, outPath: string): Promise<{ path: string; fileCount: number }> {
|
|
530
|
+
const stored = await this.readStored(name);
|
|
531
|
+
await fs.writeFile(outPath, stringifyJsonSafe(stored, { space: 2 }), 'utf8');
|
|
532
|
+
return { path: outPath, fileCount: Object.keys(stored.sections).length };
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/** 导入 Profile 文件(JSON 校验 + 存入 profiles 目录;同名已存在默认拒绝) */
|
|
536
|
+
async importProfile(filePath: string, opts: { asName?: string } = {}): Promise<ProfileMeta> {
|
|
537
|
+
let raw: string;
|
|
538
|
+
try {
|
|
539
|
+
raw = await fs.readFile(filePath, 'utf8');
|
|
540
|
+
} catch (err) {
|
|
541
|
+
throw new Error(`Profile 文件不可读: ${err instanceof Error ? err.message : String(err)}`);
|
|
542
|
+
}
|
|
543
|
+
const parsed = parseJsonSafe(raw) as StoredProfile;
|
|
544
|
+
if (parsed === null || typeof parsed !== 'object' || parsed.version !== 1 || parsed.sections === null) {
|
|
545
|
+
throw new Error('Profile 文件无效(缺少 version/sections)');
|
|
546
|
+
}
|
|
547
|
+
const name = opts.asName ?? parsed.name;
|
|
548
|
+
if (!isValidProfileName(name)) throw new Error(`非法 Profile 名: "${name}"`);
|
|
549
|
+
if (await this.exists(name)) throw new Error(`Profile "${name}" 已存在,请先删除或指定 asName`);
|
|
550
|
+
const now = new Date().toISOString();
|
|
551
|
+
const stored: StoredProfile = {
|
|
552
|
+
name,
|
|
553
|
+
version: 1,
|
|
554
|
+
createdAt: parsed.createdAt ?? now,
|
|
555
|
+
updatedAt: now,
|
|
556
|
+
sections: parsed.sections,
|
|
557
|
+
};
|
|
558
|
+
await this.writeStored(stored);
|
|
559
|
+
return this.meta(name);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/* ---------------- 纯函数辅助 ---------------- */
|
|
564
|
+
|
|
565
|
+
/** 应用用户冲突决策 + 全局策略(对齐 core/analyzer.ts applyItemResolution) */
|
|
566
|
+
function applyResolution(
|
|
567
|
+
item: PlanItem,
|
|
568
|
+
strategy: GlobalConflictStrategy,
|
|
569
|
+
resolutions: Record<string, ItemResolution>,
|
|
570
|
+
): PlanItem {
|
|
571
|
+
if (item.kind !== 'Conflict') return item;
|
|
572
|
+
const resolution = resolutions[item.id];
|
|
573
|
+
if (resolution === 'keepCurrent') {
|
|
574
|
+
return { ...item, kind: 'Skip', severity: 'info', detail: `${item.detail ?? ''}(用户选择保留当前)` };
|
|
575
|
+
}
|
|
576
|
+
if (resolution === 'useImported') {
|
|
577
|
+
return { ...item, kind: 'Update', severity: 'info', conflict: { itemId: item.id, resolution } };
|
|
578
|
+
}
|
|
579
|
+
if (strategy === 'skipExisting') {
|
|
580
|
+
return { ...item, kind: 'Skip', severity: 'info', detail: `${item.detail ?? ''}(skipExisting 策略)` };
|
|
581
|
+
}
|
|
582
|
+
if (strategy === 'replace') {
|
|
583
|
+
return { ...item, kind: 'Update', severity: 'info', detail: `${item.detail ?? ''}(replace 策略)` };
|
|
584
|
+
}
|
|
585
|
+
return item; // merge + 未决策 → 保持 Conflict(执行时跳过,报告列明)
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/** 构造切换用的最小 manifest(Profile 非 ZIP 备份,sourceZip 语义用 profile:<name>) */
|
|
589
|
+
function buildProfileManifest(ctx: HostContext): Manifest {
|
|
590
|
+
return {
|
|
591
|
+
schemaVersion: 1,
|
|
592
|
+
exporter: { name: 'DSH Config Manager', version: '0.1.0' },
|
|
593
|
+
source: {
|
|
594
|
+
dshVersion: ctx.dshVersion,
|
|
595
|
+
platform: ctx.platform as Manifest['source']['platform'],
|
|
596
|
+
arch: ctx.arch,
|
|
597
|
+
},
|
|
598
|
+
exportedAt: new Date().toISOString(),
|
|
599
|
+
sections: {
|
|
600
|
+
settings: false, ui: false, providers: false, plugins: false, mcp: false, prompts: false,
|
|
601
|
+
skills: false, agentPresets: false, workspaces: false, pluginFiles: false,
|
|
602
|
+
credentialsStatus: false, secrets: false, sessions: false,
|
|
603
|
+
},
|
|
604
|
+
security: { containsSecrets: false, encrypted: false, encryption: null },
|
|
605
|
+
};
|
|
606
|
+
}
|