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,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-import Snapshot(规范 §16/§27):只保存本次导入将被修改的目标原值。
|
|
3
|
+
* 应用层事务的基础:所有补偿动作基于快照(rollback.ts 逆序执行)。
|
|
4
|
+
*
|
|
5
|
+
* 快照不保存 credential 值(DSH 永不回读值)——credential 条目只记 existed 标志,
|
|
6
|
+
* 回滚时 existed=true 的条目如实标记「值需人工补录」。
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'node:fs/promises';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import crypto from 'node:crypto';
|
|
11
|
+
import { parseJsonSafe } from '../utils/json.ts';
|
|
12
|
+
import { sha256Hex } from '../utils/hashing.ts';
|
|
13
|
+
import type { SectionId } from '../schema/types.ts';
|
|
14
|
+
import type {
|
|
15
|
+
ConfigAdapter, HostContext, ImportPlan, PlanItem, Snapshot,
|
|
16
|
+
SnapshotEntry, SnapshotStore, SnapshotTarget,
|
|
17
|
+
} from './types.ts';
|
|
18
|
+
|
|
19
|
+
/** 导入将实际写入的目标 kinds(这些项才需要快照) */
|
|
20
|
+
const EXECUTABLE_KINDS = new Set(['Create', 'Update', 'Install', 'MissingSecret', 'MissingDependency']);
|
|
21
|
+
|
|
22
|
+
/** 文件类分区的目标基准目录(相对 homeDir;pluginFiles 的 ref 已是完整相对路径) */
|
|
23
|
+
const FILE_BASES: Partial<Record<SectionId, string>> = {
|
|
24
|
+
skills: 'skills',
|
|
25
|
+
agentPresets: '.agent-presets',
|
|
26
|
+
pluginFiles: '',
|
|
27
|
+
sessions: 'sessions',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** 解析文件类目标的绝对路径(引擎通用快照与回滚共用) */
|
|
31
|
+
export function resolveFileTarget(ctx: HostContext, adapter: SectionId, ref: string): string {
|
|
32
|
+
const base = FILE_BASES[adapter] ?? '';
|
|
33
|
+
return path.join(ctx.homeDir, base, ref);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CreateSnapshotOptions {
|
|
37
|
+
ctx: HostContext;
|
|
38
|
+
plan: ImportPlan;
|
|
39
|
+
sourceZip: string;
|
|
40
|
+
store: SnapshotStore;
|
|
41
|
+
adapters: ConfigAdapter[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 从计划中收集将被写入的 target(去重) */
|
|
45
|
+
function collectTargets(plan: ImportPlan): SnapshotTarget[] {
|
|
46
|
+
const seen = new Set<string>();
|
|
47
|
+
const targets: SnapshotTarget[] = [];
|
|
48
|
+
for (const item of plan.items) {
|
|
49
|
+
if (!EXECUTABLE_KINDS.has(item.kind) || item.target === undefined) continue;
|
|
50
|
+
const key = `${item.target.adapter}\u0000${item.target.ref}`;
|
|
51
|
+
if (seen.has(key)) continue;
|
|
52
|
+
seen.add(key);
|
|
53
|
+
targets.push(item.target);
|
|
54
|
+
}
|
|
55
|
+
return targets;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 引擎通用快照(adapter 未实现 snapshot? 时兜底) */
|
|
59
|
+
async function engineSnapshotEntry(ctx: HostContext, target: SnapshotTarget): Promise<SnapshotEntry> {
|
|
60
|
+
switch (target.adapter) {
|
|
61
|
+
case 'settings':
|
|
62
|
+
case 'ui':
|
|
63
|
+
case 'providers': {
|
|
64
|
+
try {
|
|
65
|
+
const info = await ctx.settings.describe(target.ref);
|
|
66
|
+
return { kind: 'settingsNamespace', adapter: target.adapter, ref: target.ref, before: info.value, revision: info.revision, existed: true };
|
|
67
|
+
} catch {
|
|
68
|
+
// 目标不存在该 namespace → 快照标记 existed:false(回滚时无法恢复「不存在」,如实处理)
|
|
69
|
+
return { kind: 'settingsNamespace', adapter: target.adapter, ref: target.ref, before: null, revision: 0, existed: false };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
case 'credentialsStatus': {
|
|
73
|
+
const info = await ctx.credentials.describe(target.ref);
|
|
74
|
+
return { kind: 'credential', adapter: target.adapter, ref: target.ref, before: null, existed: info.configured };
|
|
75
|
+
}
|
|
76
|
+
case 'workspaces': {
|
|
77
|
+
const records = await ctx.workspace.listRecords();
|
|
78
|
+
const rec = records.find((r) => r.id === target.ref);
|
|
79
|
+
return { kind: 'workspaceRecord', adapter: target.adapter, ref: target.ref, before: rec ?? null };
|
|
80
|
+
}
|
|
81
|
+
case 'mcp':
|
|
82
|
+
case 'plugins':
|
|
83
|
+
case 'prompts': {
|
|
84
|
+
// patchLine:从组合 patch 文件读取原行(file 为必填的 file 字段约定为 'cordis.patch.yml')
|
|
85
|
+
const file = 'cordis.patch.yml';
|
|
86
|
+
const lines = await ctx.patchFile.readPatchLines(file);
|
|
87
|
+
const line = lines.find((l) => l.lineId === target.ref);
|
|
88
|
+
return { kind: 'patchLine', adapter: target.adapter, ref: target.ref, before: line?.raw ?? null, existed: line !== undefined };
|
|
89
|
+
}
|
|
90
|
+
case 'skills':
|
|
91
|
+
case 'agentPresets':
|
|
92
|
+
case 'pluginFiles':
|
|
93
|
+
case 'sessions': {
|
|
94
|
+
const abs = resolveFileTarget(ctx, target.adapter, target.ref);
|
|
95
|
+
if (!(await ctx.fs.exists(abs))) {
|
|
96
|
+
return { kind: 'file', adapter: target.adapter, ref: target.ref, before: null, existed: false };
|
|
97
|
+
}
|
|
98
|
+
const data = await ctx.fs.readFile(abs);
|
|
99
|
+
// 文件字节不放进 SnapshotEntry(契约纯净),由 createSnapshot 收集进 blobs Map 统一落盘
|
|
100
|
+
return {
|
|
101
|
+
kind: 'file',
|
|
102
|
+
adapter: target.adapter,
|
|
103
|
+
ref: target.ref,
|
|
104
|
+
before: { contentHash: sha256Hex(data) },
|
|
105
|
+
existed: true,
|
|
106
|
+
copiedTo: `blobs/${crypto.randomUUID()}`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
default:
|
|
110
|
+
return { kind: 'settingsNamespace', adapter: target.adapter, ref: target.ref, before: null };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 生成并落盘快照:只覆盖将被写入的目标。
|
|
116
|
+
* 文件字节经 blobs Map 交给 store.save(SnapshotEntry 契约不含二进制)。
|
|
117
|
+
*/
|
|
118
|
+
export async function createSnapshot(opts: CreateSnapshotOptions): Promise<Snapshot> {
|
|
119
|
+
const { ctx, plan, sourceZip, store, adapters } = opts;
|
|
120
|
+
const targets = collectTargets(plan);
|
|
121
|
+
|
|
122
|
+
const entries: SnapshotEntry[] = [];
|
|
123
|
+
const blobs = new Map<string, Uint8Array>();
|
|
124
|
+
|
|
125
|
+
// 1) 有 adapter.snapshot? 的优先(adapter 更懂自己的数据)
|
|
126
|
+
const byAdapter = new Map<SectionId, SnapshotTarget[]>();
|
|
127
|
+
for (const t of targets) {
|
|
128
|
+
const list = byAdapter.get(t.adapter) ?? [];
|
|
129
|
+
list.push(t);
|
|
130
|
+
byAdapter.set(t.adapter, list);
|
|
131
|
+
}
|
|
132
|
+
for (const adapter of adapters) {
|
|
133
|
+
const adapterTargets = byAdapter.get(adapter.id);
|
|
134
|
+
if (!adapterTargets || adapterTargets.length === 0) continue;
|
|
135
|
+
if (adapter.snapshot) {
|
|
136
|
+
const adapterEntries = await adapter.snapshot(adapterTargets, ctx);
|
|
137
|
+
entries.push(...adapterEntries);
|
|
138
|
+
byAdapter.delete(adapter.id);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 2) 引擎通用快照兜底
|
|
143
|
+
for (const [adapterId, remaining] of byAdapter) {
|
|
144
|
+
for (const target of remaining) {
|
|
145
|
+
const entry = await engineSnapshotEntry(ctx, target);
|
|
146
|
+
// 文件字节:文件类条目在引擎快照里读出内容 → blobs
|
|
147
|
+
if (entry.kind === 'file' && entry.copiedTo && entry.existed) {
|
|
148
|
+
const abs = resolveFileTarget(ctx, adapterId, target.ref);
|
|
149
|
+
blobs.set(entry.copiedTo, await ctx.fs.readFile(abs));
|
|
150
|
+
}
|
|
151
|
+
entries.push(entry);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const snapshot: Snapshot = {
|
|
156
|
+
id: crypto.randomUUID(),
|
|
157
|
+
createdAt: new Date().toISOString(),
|
|
158
|
+
sourceZip,
|
|
159
|
+
entries,
|
|
160
|
+
};
|
|
161
|
+
// file 条目登记 snapshotId,回滚读 blob 时定位快照目录
|
|
162
|
+
for (const entry of snapshot.entries) {
|
|
163
|
+
if (entry.kind === 'file') entry.snapshotId = snapshot.id;
|
|
164
|
+
}
|
|
165
|
+
await store.save(snapshot, blobs);
|
|
166
|
+
return snapshot;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* ---------------- 默认文件快照存储 ---------------- */
|
|
170
|
+
|
|
171
|
+
export interface FileSnapshotStoreOptions {
|
|
172
|
+
/** 快照根目录(宿主决定,如 ~/.dsh/dsh-config-manager/snapshots) */
|
|
173
|
+
dir: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** 文件快照存储:<dir>/<id>/snapshot.json + <dir>/<id>/blobs/* */
|
|
177
|
+
export class FileSnapshotStore implements SnapshotStore {
|
|
178
|
+
private readonly options: FileSnapshotStoreOptions;
|
|
179
|
+
|
|
180
|
+
constructor(options: FileSnapshotStoreOptions) {
|
|
181
|
+
this.options = options;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private snapshotDir(id: string): string {
|
|
185
|
+
return path.join(this.options.dir, id);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async save(snapshot: Snapshot, blobs: Map<string, Uint8Array> = new Map()): Promise<string> {
|
|
189
|
+
const dir = this.snapshotDir(snapshot.id);
|
|
190
|
+
await fs.mkdir(path.join(dir, 'blobs'), { recursive: true });
|
|
191
|
+
for (const [blobPath, data] of blobs) {
|
|
192
|
+
const target = path.join(dir, blobPath);
|
|
193
|
+
if (!target.startsWith(dir)) throw new Error(`快照 blob 路径越界: ${blobPath}`);
|
|
194
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
195
|
+
await fs.writeFile(target, data);
|
|
196
|
+
}
|
|
197
|
+
await fs.writeFile(path.join(dir, 'snapshot.json'), JSON.stringify(snapshot, null, 2));
|
|
198
|
+
return snapshot.id;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async load(id: string): Promise<Snapshot> {
|
|
202
|
+
const raw = await fs.readFile(path.join(this.snapshotDir(id), 'snapshot.json'), 'utf8');
|
|
203
|
+
return parseJsonSafe(raw) as Snapshot;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async readBlob(id: string, blobPath: string): Promise<Uint8Array> {
|
|
207
|
+
const target = path.join(this.snapshotDir(id), blobPath);
|
|
208
|
+
const dir = this.options.dir;
|
|
209
|
+
if (!target.startsWith(dir)) throw new Error(`快照 blob 路径越界: ${blobPath}`);
|
|
210
|
+
return fs.readFile(target);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export 编排(规范 §1/§18/§21,设计 §4.4):
|
|
3
|
+
* adapter 收集各分区 → Secret 过滤 → manifest → checksum → ZIP。
|
|
4
|
+
*
|
|
5
|
+
* 安全不变量:
|
|
6
|
+
* - Secret 值默认永不进入导出数据(结构化分区逐一过 SecretScanner);
|
|
7
|
+
* - includeSecrets=true 必须注入 EncryptionProvider(m4 实现),否则拒绝导出;
|
|
8
|
+
* - 加密密码/秘密值绝不写入 manifest 与日志。
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'node:fs/promises';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { buildChecksums } from '../utils/hashing.ts';
|
|
13
|
+
import { stringifyJsonSafe } from '../utils/json.ts';
|
|
14
|
+
import { buildManifest, CHECKSUMS_FILE, MANIFEST_FILE, EXPORTER_NAME } from '../schema/manifest.ts';
|
|
15
|
+
import { SECTION_JSON_PATHS, SECTION_FILE_PREFIXES, isFileSection } from '../schema/config.ts';
|
|
16
|
+
import { writeZip } from '../utils/zip.ts';
|
|
17
|
+
import type { Manifest, SectionId } from '../schema/types.ts';
|
|
18
|
+
import type {
|
|
19
|
+
ConfigAdapter, EncryptionProvider, ExportOptions, ExportReport,
|
|
20
|
+
ExportSection, HostContext, SecretScanner, SensitiveHit,
|
|
21
|
+
} from './types.ts';
|
|
22
|
+
|
|
23
|
+
export interface ExporterOptions {
|
|
24
|
+
ctx: HostContext;
|
|
25
|
+
adapters: ConfigAdapter[];
|
|
26
|
+
/** Secret 扫描器;缺省用字段名黑名单剥离(m4 可注入强化版) */
|
|
27
|
+
scanner?: SecretScanner;
|
|
28
|
+
/** 加密提供者(m4 用 node:crypto 实现);includeSecrets 时必填 */
|
|
29
|
+
encryption?: EncryptionProvider | null;
|
|
30
|
+
/** 插件自身版本(manifest.exporter.version) */
|
|
31
|
+
exporterVersion?: string;
|
|
32
|
+
now?: () => Date;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 缺省 SecretScanner:递归黑名单字段剥离(字段名大小写不敏感;二进制/Uint8Array 原样跳过) */
|
|
36
|
+
export function defaultSecretScanner(): SecretScanner {
|
|
37
|
+
const SENSITIVE_FIELDS = [
|
|
38
|
+
'password', 'passwd', 'token', 'accesstoken', 'refreshtoken', 'apikey',
|
|
39
|
+
'secret', 'credential', 'authorization', 'cookie', 'privatekey', 'clientsecret',
|
|
40
|
+
];
|
|
41
|
+
/** 仅存「引用名」而非值的字段(如 apiKeyEnv=DEEPSEEK_API_KEY 是环境变量名,不是秘密) */
|
|
42
|
+
const REFERENCE_FIELDS = new Set([
|
|
43
|
+
'apikeyenv', 'api_key_env', 'apikeyname', 'tokenenv', 'accesstokenenv',
|
|
44
|
+
'refreshtokenenv', 'clientsecretenv', 'passwordenv',
|
|
45
|
+
]);
|
|
46
|
+
return {
|
|
47
|
+
scanAndRedact(data: unknown): { sanitized: unknown; hits: SensitiveHit[] } {
|
|
48
|
+
const hits: SensitiveHit[] = [];
|
|
49
|
+
const walk = (v: unknown, p: string): unknown => {
|
|
50
|
+
if (v === null || typeof v !== 'object') return v;
|
|
51
|
+
if (v instanceof Uint8Array) return v; // 二进制内容不按字段展开
|
|
52
|
+
if (Array.isArray(v)) return v.map((item, i) => walk(item, `${p}[${i}]`));
|
|
53
|
+
const out: Record<string, unknown> = {};
|
|
54
|
+
for (const [k, val] of Object.entries(v as Record<string, unknown>)) {
|
|
55
|
+
const lower = k.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
56
|
+
const isRefName = REFERENCE_FIELDS.has(lower);
|
|
57
|
+
const sensitive = !isRefName && SENSITIVE_FIELDS.some((f) => lower.includes(f));
|
|
58
|
+
if (sensitive && typeof val === 'string' && val !== '') {
|
|
59
|
+
hits.push({ path: p === '' ? k : `${p}.${k}`, field: k });
|
|
60
|
+
out[k] = ''; // 值剥离为空串(保留字段名与位置,供「需补录」提示)
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
out[k] = walk(val, p === '' ? k : `${p}.${k}`);
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
};
|
|
67
|
+
return { sanitized: walk(data, ''), hits };
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class Exporter {
|
|
73
|
+
private readonly ctx: HostContext;
|
|
74
|
+
private readonly adapters: ConfigAdapter[];
|
|
75
|
+
private readonly scanner: SecretScanner;
|
|
76
|
+
private readonly encryption: EncryptionProvider | null;
|
|
77
|
+
private readonly exporterVersion: string;
|
|
78
|
+
private readonly now: () => Date;
|
|
79
|
+
|
|
80
|
+
constructor(opts: ExporterOptions) {
|
|
81
|
+
this.ctx = opts.ctx;
|
|
82
|
+
this.adapters = opts.adapters;
|
|
83
|
+
this.scanner = opts.scanner ?? defaultSecretScanner();
|
|
84
|
+
this.encryption = opts.encryption ?? null;
|
|
85
|
+
this.exporterVersion = opts.exporterVersion ?? '0.1.0';
|
|
86
|
+
this.now = opts.now ?? (() => new Date());
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 导出:收集 → 过滤 → checksum → manifest → ZIP。
|
|
91
|
+
* 返回 zipPath(含文件名)、manifest、报告。
|
|
92
|
+
*/
|
|
93
|
+
async export(options: ExportOptions): Promise<{ zipPath: string; manifest: Manifest; report: ExportReport }> {
|
|
94
|
+
const { includeSecrets, only } = options;
|
|
95
|
+
if (includeSecrets && !this.encryption) {
|
|
96
|
+
throw new Error('导出包含秘密需要注入 EncryptionProvider(m4 实现),拒绝明文导出秘密');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 1. 选定分区(only 过滤 + 默认包含)
|
|
100
|
+
const selected = this.adapters
|
|
101
|
+
.filter((a) => (only === undefined ? a.defaultIncluded : only.includes(a.id)))
|
|
102
|
+
.map((a) => a.id);
|
|
103
|
+
|
|
104
|
+
// 2. 逐 adapter 收集(导出数据)
|
|
105
|
+
const sections: ExportSection[] = [];
|
|
106
|
+
const warnings: string[] = [];
|
|
107
|
+
const redactedHits: SensitiveHit[] = [];
|
|
108
|
+
const included: ExportReport['included'] = [];
|
|
109
|
+
const excluded: SectionId[] = this.adapters.filter((a) => !selected.includes(a.id)).map((a) => a.id);
|
|
110
|
+
|
|
111
|
+
for (const adapter of this.adapters) {
|
|
112
|
+
if (!selected.includes(adapter.id)) continue;
|
|
113
|
+
let section: ExportSection;
|
|
114
|
+
try {
|
|
115
|
+
section = await adapter.export(this.ctx, options);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
// 单个分区失败不拖垮整体(§34.17);如实告警并跳过
|
|
118
|
+
warnings.push(`分区 ${adapter.id} 导出失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
119
|
+
excluded.push(adapter.id);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
// 3. Secret 过滤:结构化数据逐一过 scanner;
|
|
123
|
+
// 文件类分区(skills 等)内容为自由文本,扫描语义由 m4 强化,此处不动
|
|
124
|
+
let sanitized = section.data;
|
|
125
|
+
if (!isFileSection(adapter.id)) {
|
|
126
|
+
const scanned = this.scanner.scanAndRedact(section.data);
|
|
127
|
+
redactedHits.push(...scanned.hits);
|
|
128
|
+
sanitized = scanned.sanitized;
|
|
129
|
+
}
|
|
130
|
+
sections.push({ ...section, data: sanitized });
|
|
131
|
+
included.push({ section: adapter.id, counts: section.counts });
|
|
132
|
+
warnings.push(...section.warnings);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 4. 组装 ZIP 条目(JSON 分区 + 文件类分区 + secrets.enc + checksums + manifest)
|
|
136
|
+
const entries: { name: string; data: Uint8Array }[] = [];
|
|
137
|
+
const sectionFlags = buildSectionFlags(sections);
|
|
138
|
+
let containsSecrets = false;
|
|
139
|
+
let encrypted = false;
|
|
140
|
+
let encryption: Manifest['security']['encryption'] = null;
|
|
141
|
+
|
|
142
|
+
for (const section of sections) {
|
|
143
|
+
if (isFileSection(section.sectionId)) {
|
|
144
|
+
const prefix = SECTION_FILE_PREFIXES[section.sectionId]!;
|
|
145
|
+
const files = (section.data as { files?: { relativePath: string; data: Uint8Array }[] }).files ?? [];
|
|
146
|
+
for (const file of files) {
|
|
147
|
+
entries.push({ name: `${prefix}${file.relativePath}`, data: file.data });
|
|
148
|
+
}
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const jsonPath = SECTION_JSON_PATHS[section.sectionId];
|
|
152
|
+
if (jsonPath === undefined) continue;
|
|
153
|
+
entries.push({
|
|
154
|
+
name: jsonPath,
|
|
155
|
+
data: Buffer.from(stringifyJsonSafe(section.data, { space: 2 }), 'utf8'),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// secrets.enc:仅加密备份
|
|
160
|
+
if (includeSecrets && this.encryption) {
|
|
161
|
+
const credentialsFile = path.join(this.ctx.homeDir, '.credentials.yaml');
|
|
162
|
+
let plaintext: string;
|
|
163
|
+
try {
|
|
164
|
+
const raw = await this.ctx.fs.readFile(credentialsFile);
|
|
165
|
+
plaintext = Buffer.from(raw).toString('utf8');
|
|
166
|
+
} catch (err) {
|
|
167
|
+
warnings.push(`读取凭据文件失败,跳过秘密导出: ${err instanceof Error ? err.message : String(err)}`);
|
|
168
|
+
plaintext = '';
|
|
169
|
+
}
|
|
170
|
+
if (plaintext !== '') {
|
|
171
|
+
const result = await this.encryption.encrypt(plaintext);
|
|
172
|
+
entries.push({ name: 'security/secrets.enc', data: result.blob });
|
|
173
|
+
encryption = result.info;
|
|
174
|
+
containsSecrets = true;
|
|
175
|
+
encrypted = true;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 5. checksums(覆盖除 manifest/checksums 外的全部条目)
|
|
180
|
+
const checksums = buildChecksums(entries);
|
|
181
|
+
entries.push({ name: CHECKSUMS_FILE, data: Buffer.from(stringifyJsonSafe(checksums, { space: 2 }), 'utf8') });
|
|
182
|
+
|
|
183
|
+
// 6. manifest(最后写:需要完整分区与安全信息)
|
|
184
|
+
const manifest = buildManifest({
|
|
185
|
+
exporterVersion: this.exporterVersion,
|
|
186
|
+
dshVersion: this.ctx.dshVersion,
|
|
187
|
+
platform: this.ctx.platform as Manifest['source']['platform'],
|
|
188
|
+
arch: this.ctx.arch,
|
|
189
|
+
sections: sectionFlags,
|
|
190
|
+
containsSecrets,
|
|
191
|
+
encrypted,
|
|
192
|
+
encryption,
|
|
193
|
+
exportedAt: this.now().toISOString(),
|
|
194
|
+
});
|
|
195
|
+
entries.push({ name: MANIFEST_FILE, data: Buffer.from(stringifyJsonSafe(manifest, { space: 2 }), 'utf8') });
|
|
196
|
+
|
|
197
|
+
// 7. 落盘
|
|
198
|
+
const outPath = options.outPath ?? defaultOutPath(this.now());
|
|
199
|
+
await writeZip(outPath, entries);
|
|
200
|
+
const stat = await fs.stat(outPath);
|
|
201
|
+
|
|
202
|
+
// 日志不泄值:只记分区与命中数量
|
|
203
|
+
this.ctx.log.info('导出完成', {
|
|
204
|
+
file: path.basename(outPath),
|
|
205
|
+
sizeBytes: stat.size,
|
|
206
|
+
sections: Object.keys(sectionFlags).filter((k) => sectionFlags[k as SectionId]),
|
|
207
|
+
redactedFields: redactedHits.length,
|
|
208
|
+
containsSecrets,
|
|
209
|
+
encrypted,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const report: ExportReport = {
|
|
213
|
+
included,
|
|
214
|
+
excluded,
|
|
215
|
+
security: {
|
|
216
|
+
secretsExcluded: !includeSecrets,
|
|
217
|
+
containsSecrets,
|
|
218
|
+
encrypted,
|
|
219
|
+
redactedHits: redactedHits.length,
|
|
220
|
+
},
|
|
221
|
+
file: { name: path.basename(outPath), sizeBytes: stat.size },
|
|
222
|
+
warnings,
|
|
223
|
+
};
|
|
224
|
+
return { zipPath: outPath, manifest, report };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** 构建 manifest.sections 布尔表(只含实际导出分区) */
|
|
229
|
+
function buildSectionFlags(sections: ExportSection[]): Manifest['sections'] {
|
|
230
|
+
const flags = {} as Manifest['sections'];
|
|
231
|
+
for (const id of Object.keys(SECTION_JSON_PATHS) as SectionId[]) flags[id] = false;
|
|
232
|
+
for (const id of Object.keys(SECTION_FILE_PREFIXES) as SectionId[]) flags[id] = false;
|
|
233
|
+
flags['secrets'] = false;
|
|
234
|
+
for (const section of sections) flags[section.sectionId] = true;
|
|
235
|
+
return flags;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function defaultOutPath(now: Date): string {
|
|
239
|
+
const y = now.getFullYear();
|
|
240
|
+
const m = String(now.getMonth() + 1).padStart(2, '0');
|
|
241
|
+
const d = String(now.getDate()).padStart(2, '0');
|
|
242
|
+
return `dsh-config-${y}-${m}-${d}.zip`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** 供报告使用:导出器身份(避免与 manifest 常量重复维护) */
|
|
246
|
+
export const EXPORTER_INFO = { name: EXPORTER_NAME };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Importer 门面:封装导入 14 步流程(规范 §9),委托 Analyzer 三段式。
|
|
3
|
+
*
|
|
4
|
+
* 1 选 ZIP → 2 校验 ZIP → 3 读 manifest → 4 完整性 → 5 schema → 6 兼容
|
|
5
|
+
* → 7 扫描 → 8 ImportPlan → 9 Preview(Dry Run) → 10 用户确认
|
|
6
|
+
* → 11 快照 → 12 执行 → 13 校验 → 14 结果(失败则回滚)
|
|
7
|
+
*
|
|
8
|
+
* 安全阀:executeImportPlan 必须显式 confirm,否则拒绝(ImportNotConfirmedError)。
|
|
9
|
+
* Dry Run:analyzeImport / createImportPlan 零写入(ZIP 全程内存解析,不进磁盘)。
|
|
10
|
+
*/
|
|
11
|
+
import type { ZipArchive, ZipSafetyLimits } from '../utils/zip.ts';
|
|
12
|
+
import { Analyzer } from './analyzer.ts';
|
|
13
|
+
import type {
|
|
14
|
+
ConfigAdapter, HostContext, ImportAnalysis, ImportDecisions, ImportPlan,
|
|
15
|
+
ImportResult, SnapshotStore,
|
|
16
|
+
} from './types.ts';
|
|
17
|
+
|
|
18
|
+
export interface ImporterOptions {
|
|
19
|
+
ctx: HostContext;
|
|
20
|
+
adapters: ConfigAdapter[];
|
|
21
|
+
snapshotStore: SnapshotStore;
|
|
22
|
+
limits?: ZipSafetyLimits;
|
|
23
|
+
/** 依赖存在性检查器(缺省不检查) */
|
|
24
|
+
dependencyChecker?: (command: string) => Promise<boolean>;
|
|
25
|
+
/** m4 可注入强化版 ZIP 安全解析 */
|
|
26
|
+
parseZipOverride?: (buf: Uint8Array, limits?: ZipSafetyLimits) => ZipArchive;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ExecuteOptions {
|
|
30
|
+
/** 用户确认(UI 收集);非 true 一律拒绝执行 */
|
|
31
|
+
confirm: boolean;
|
|
32
|
+
/** 用户补录的秘密值(仅内存) */
|
|
33
|
+
secretInputs?: Record<string, string>;
|
|
34
|
+
/** 加密备份解密结果(仅内存;解密必须经 m4 encryption provider) */
|
|
35
|
+
decryptedCredentials?: Map<string, string>;
|
|
36
|
+
/** 任一项失败立即整体回滚(默认 false:单项失败如实记录并继续其余项) */
|
|
37
|
+
rollbackOnError?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class Importer {
|
|
41
|
+
private readonly analyzer: Analyzer;
|
|
42
|
+
|
|
43
|
+
constructor(opts: ImporterOptions) {
|
|
44
|
+
this.analyzer = new Analyzer(opts);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 步骤 1-8 + 分析:只读,返回 ImportAnalysis(含兼容性/路径/依赖/秘密摘要) */
|
|
48
|
+
analyzeImport(zipPath: string): Promise<ImportAnalysis> {
|
|
49
|
+
return this.analyzer.analyzeImport(zipPath);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 步骤 9:Dry Run / Preview —— 合并用户冲突决策与路径映射,输出最终可执行计划(零写入) */
|
|
53
|
+
createImportPlan(zipPath: string, decisions: ImportDecisions): Promise<ImportPlan> {
|
|
54
|
+
return this.analyzer.createImportPlan(zipPath, decisions);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 步骤 10-14:确认 → 快照 → 执行 → 校验 → 结果;失败可整体回滚并如实报告 */
|
|
58
|
+
executeImportPlan(zipPath: string, plan: ImportPlan, opts: ExecuteOptions): Promise<ImportResult> {
|
|
59
|
+
return this.analyzer.executeImportPlan(zipPath, plan, {
|
|
60
|
+
confirm: opts.confirm,
|
|
61
|
+
secretInputs: opts.secretInputs,
|
|
62
|
+
decryptedCredentials: opts.decryptedCredentials,
|
|
63
|
+
rollbackOnError: opts.rollbackOnError,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 核心引擎公共出口:m5(适配器)、m6(UI)、m7(测试)与宿主插件统一从这里引用。
|
|
3
|
+
*/
|
|
4
|
+
export {
|
|
5
|
+
Exporter, defaultSecretScanner, EXPORTER_INFO,
|
|
6
|
+
type ExporterOptions,
|
|
7
|
+
} from './exporter.ts';
|
|
8
|
+
export {
|
|
9
|
+
Importer, type ImporterOptions, type ExecuteOptions,
|
|
10
|
+
} from './importer.ts';
|
|
11
|
+
export {
|
|
12
|
+
Analyzer, type AnalyzerOptions,
|
|
13
|
+
} from './analyzer.ts';
|
|
14
|
+
export {
|
|
15
|
+
createSnapshot, FileSnapshotStore, resolveFileTarget,
|
|
16
|
+
type CreateSnapshotOptions, type FileSnapshotStoreOptions,
|
|
17
|
+
} from './backup.ts';
|
|
18
|
+
export { rollback, type RollbackOptions } from './rollback.ts';
|
|
19
|
+
export {
|
|
20
|
+
computeCompatibility, describeCompatibility, describeSchemaStatus,
|
|
21
|
+
validateSections,
|
|
22
|
+
} from './validator.ts';
|
|
23
|
+
export {
|
|
24
|
+
ImportNotConfirmedError, ImportFailedError,
|
|
25
|
+
} from './types.ts';
|
|
26
|
+
export type * from './types.ts';
|
|
27
|
+
|
|
28
|
+
/* —— 类型与工具的便捷重导出 —— */
|
|
29
|
+
export type {
|
|
30
|
+
ExportOptions, ExportSection, ValidationResult, HostContext,
|
|
31
|
+
SettingsFacade, CredentialsFacade, PluginsFacade, WorkspaceFacade,
|
|
32
|
+
PatchFileFacade, FileSystemFacade, NamespaceInfo, PluginInfo,
|
|
33
|
+
ConfigAdapter, Portability, SecretScanner, SensitiveHit, EncryptionProvider,
|
|
34
|
+
PlanItem, PlanItemKind, ItemResolution, GlobalConflictStrategy,
|
|
35
|
+
ImportAnalysis, ImportDecisions, ImportPlan, ImportResult, ExecutedItem,
|
|
36
|
+
ImportContext, SnapshotTarget, SnapshotEntry, Snapshot, SnapshotStore,
|
|
37
|
+
RollbackReport, PathMapping, PathIssue, CompatibilityInput,
|
|
38
|
+
CompatibilityScore, ExportReport, ApplyResult, ConflictDecision,
|
|
39
|
+
} from './types.ts';
|