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,631 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 三段式核心(对齐设计 §5.1/§13.5):
|
|
3
|
+
* analyzeImport() → ImportAnalysis(纯计算,零写入)
|
|
4
|
+
* createImportPlan() → ImportPlan(汇总 PlanItem + 冲突决策 + 路径映射;纯计算)
|
|
5
|
+
* executeImportPlan() → ImportResult(快照 → 分阶段 apply → 校验 → 结果/回滚)
|
|
6
|
+
*
|
|
7
|
+
* Dry Run 保证:analyzeImport / createImportPlan 除读取 ZIP 字节外不做任何系统修改,
|
|
8
|
+
* ZIP 以内存方式解析(不进磁盘),杜绝不可信输入在分析阶段落盘。
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'node:fs/promises';
|
|
11
|
+
import { sha256Hex } from '../utils/hashing.ts';
|
|
12
|
+
import { parseJsonSafe } from '../utils/json.ts';
|
|
13
|
+
import {
|
|
14
|
+
SECTION_FILE_PREFIXES, SECTION_JSON_PATHS, isFileSection, validateSectionData,
|
|
15
|
+
} from '../schema/config.ts';
|
|
16
|
+
import { CHECKSUMS_FILE, MANIFEST_FILE, parseManifest } from '../schema/manifest.ts';
|
|
17
|
+
import { canImport, describeVersion, isSupported } from '../schema/versions.ts';
|
|
18
|
+
import { isAbsolutePath, applyPrefixMappings } from '../utils/paths.ts';
|
|
19
|
+
import { parseZip, type ZipArchive, type ZipSafetyLimits } from '../utils/zip.ts';
|
|
20
|
+
import type { FilesSection, Manifest, SectionId } from '../schema/types.ts';
|
|
21
|
+
import { createSnapshot } from './backup.ts';
|
|
22
|
+
import { rollback } from './rollback.ts';
|
|
23
|
+
import { computeCompatibility } from './validator.ts';
|
|
24
|
+
import {
|
|
25
|
+
ImportNotConfirmedError, type ApplyResult, type ConfigAdapter, type ExecutedItem,
|
|
26
|
+
type HostContext, type ImportAnalysis, type ImportContext, type ImportDecisions,
|
|
27
|
+
type ImportPlan, type ImportResult, type PathIssue, type PathMapping, type PlanItem,
|
|
28
|
+
type SnapshotStore,
|
|
29
|
+
} from './types.ts';
|
|
30
|
+
|
|
31
|
+
/** 执行阶段顺序(设计 §5.4:副作用大的 patch/安装最后) */
|
|
32
|
+
const APPLY_ORDER: readonly SectionId[] = [
|
|
33
|
+
'settings', 'ui', 'providers', 'prompts', 'skills', 'agentPresets',
|
|
34
|
+
'workspaces', 'pluginFiles', 'mcp', 'plugins', 'credentialsStatus',
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
/** ZIP 内可执行文件扩展名黑名单(§19.6:只警告,本插件不执行任何脚本) */
|
|
38
|
+
const EXECUTABLE_EXTENSIONS = new Set(['.exe', '.bat', '.cmd', '.sh', '.ps1', '.dll', '.so', '.dylib', '.bin', '.jar']);
|
|
39
|
+
|
|
40
|
+
/** 已知外部依赖(MCP command 检测用,§15) */
|
|
41
|
+
const KNOWN_DEPENDENCIES = new Set([
|
|
42
|
+
'npx', 'node', 'npm', 'pnpm', 'yarn', 'bun', 'python', 'python3', 'pip', 'pip3',
|
|
43
|
+
'uv', 'git', 'docker', 'rg', 'bash', 'zsh', 'code', 'cargo', 'go',
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
export interface AnalyzerOptions {
|
|
47
|
+
ctx: HostContext;
|
|
48
|
+
adapters: ConfigAdapter[];
|
|
49
|
+
snapshotStore: SnapshotStore;
|
|
50
|
+
limits?: ZipSafetyLimits;
|
|
51
|
+
/** 依赖存在性检查器(缺省不检查;m5/宿主可注入 which 类实现) */
|
|
52
|
+
dependencyChecker?: (command: string) => Promise<boolean>;
|
|
53
|
+
/** m4 可注入强化版 ZIP 安全解析 */
|
|
54
|
+
parseZipOverride?: (buf: Uint8Array, limits?: ZipSafetyLimits) => ZipArchive;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface Bundle {
|
|
58
|
+
archive: ZipArchive;
|
|
59
|
+
manifest: Manifest;
|
|
60
|
+
checksums: { ok: boolean; mismatches: string[]; missing: string[] };
|
|
61
|
+
zipWarnings: string[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface AnalyzedBundle extends Bundle {
|
|
65
|
+
sections: Map<SectionId, unknown>;
|
|
66
|
+
adapterItems: PlanItem[];
|
|
67
|
+
adapterIssues: string[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class Analyzer {
|
|
71
|
+
private readonly ctx: HostContext;
|
|
72
|
+
private readonly adapters: ConfigAdapter[];
|
|
73
|
+
private readonly snapshotStore: SnapshotStore;
|
|
74
|
+
private readonly limits?: ZipSafetyLimits;
|
|
75
|
+
private readonly dependencyChecker?: (command: string) => Promise<boolean>;
|
|
76
|
+
private readonly parseZipFn: (buf: Uint8Array, limits?: ZipSafetyLimits) => ZipArchive;
|
|
77
|
+
/** 会话内 bundle 缓存(zipPath → 解析结果),避免重复解压 */
|
|
78
|
+
private readonly bundleCache = new Map<string, Bundle>();
|
|
79
|
+
|
|
80
|
+
constructor(opts: AnalyzerOptions) {
|
|
81
|
+
this.ctx = opts.ctx;
|
|
82
|
+
this.adapters = opts.adapters;
|
|
83
|
+
this.snapshotStore = opts.snapshotStore;
|
|
84
|
+
this.limits = opts.limits;
|
|
85
|
+
this.dependencyChecker = opts.dependencyChecker;
|
|
86
|
+
this.parseZipFn = opts.parseZipOverride ?? parseZip;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* ---------------- 第 1-6 步:ZIP 读入 → 安全解析 → manifest → 完整性 → schema ---------------- */
|
|
90
|
+
|
|
91
|
+
private async loadBundle(zipPath: string): Promise<Bundle> {
|
|
92
|
+
const cached = this.bundleCache.get(zipPath);
|
|
93
|
+
if (cached) return cached;
|
|
94
|
+
|
|
95
|
+
// 1. 选 ZIP(存在性)
|
|
96
|
+
let raw: Uint8Array;
|
|
97
|
+
try {
|
|
98
|
+
raw = await fs.readFile(zipPath);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
throw new Error(`无法读取备份文件 "${zipPath}": ${err instanceof Error ? err.message : String(err)}`);
|
|
101
|
+
}
|
|
102
|
+
// 2. 校验 ZIP(安全解析:条目名/数量/体积上限)
|
|
103
|
+
const archive = this.parseZipFn(raw, this.limits);
|
|
104
|
+
|
|
105
|
+
// 3. manifest
|
|
106
|
+
if (!archive.has(MANIFEST_FILE)) throw new Error('备份缺少 manifest.json,不是有效的 DSH Config Manager 备份');
|
|
107
|
+
let manifest: Manifest;
|
|
108
|
+
try {
|
|
109
|
+
manifest = parseManifest(archive.readEntryText(MANIFEST_FILE));
|
|
110
|
+
} catch (err) {
|
|
111
|
+
throw new Error(`manifest.json 解析失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 4. 完整性(integrity/checksums.json 逐一 SHA-256)
|
|
115
|
+
let checksums = { ok: true, mismatches: [] as string[], missing: [] as string[] };
|
|
116
|
+
if (archive.has(CHECKSUMS_FILE)) {
|
|
117
|
+
const table = parseJsonSafe(archive.readEntryText(CHECKSUMS_FILE)) as Record<string, string>;
|
|
118
|
+
const entries = new Map<string, Uint8Array>();
|
|
119
|
+
for (const name of archive.names()) {
|
|
120
|
+
if (name === MANIFEST_FILE || name === CHECKSUMS_FILE) continue;
|
|
121
|
+
try {
|
|
122
|
+
entries.set(name, archive.readEntry(name));
|
|
123
|
+
} catch {
|
|
124
|
+
entries.delete(name); // 损坏条目在完整性阶段即失败
|
|
125
|
+
checksums.ok = false;
|
|
126
|
+
checksums.mismatches.push(name);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const result = await verifyAgainstTable(entries, table);
|
|
130
|
+
checksums = result;
|
|
131
|
+
if (!checksums.ok) {
|
|
132
|
+
throw new Error(`备份完整性校验失败: ${[...checksums.mismatches, ...checksums.missing].map((m) => `"${m}"`).join(', ')}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 5. schema 版本判定(集中)
|
|
137
|
+
if (!isSupported(manifest.schemaVersion)) {
|
|
138
|
+
throw new Error(`备份 ${describeVersion(manifest.schemaVersion)},无法导入(当前支持 v${1})`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 6. 扫描:可执行文件条目 → 警告(不执行)
|
|
142
|
+
const zipWarnings: string[] = [];
|
|
143
|
+
for (const name of archive.names()) {
|
|
144
|
+
const ext = name.slice(name.lastIndexOf('.')).toLowerCase();
|
|
145
|
+
if (EXECUTABLE_EXTENSIONS.has(ext)) {
|
|
146
|
+
zipWarnings.push(`备份包含可执行文件 "${name}",本插件不会执行它`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const bundle: Bundle = { archive, manifest, checksums, zipWarnings };
|
|
151
|
+
this.bundleCache.set(zipPath, bundle);
|
|
152
|
+
return bundle;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* ---------------- 第 7 步:分区内容扫描/提取 ---------------- */
|
|
156
|
+
|
|
157
|
+
private extractSections(bundle: Bundle): Map<SectionId, unknown> {
|
|
158
|
+
const { archive, manifest } = bundle;
|
|
159
|
+
const sections = new Map<SectionId, unknown>();
|
|
160
|
+
for (const [sectionId, enabled] of Object.entries(manifest.sections) as [SectionId, boolean][]) {
|
|
161
|
+
if (!enabled) continue;
|
|
162
|
+
if (isFileSection(sectionId)) {
|
|
163
|
+
const prefix = SECTION_FILE_PREFIXES[sectionId]!;
|
|
164
|
+
const files: FilesSection['files'] = [];
|
|
165
|
+
for (const name of archive.names()) {
|
|
166
|
+
if (!name.startsWith(prefix) || name === prefix) continue;
|
|
167
|
+
const rel = name.slice(prefix.length);
|
|
168
|
+
if (rel === '' || rel.endsWith('/')) continue;
|
|
169
|
+
const data = archive.readEntry(name);
|
|
170
|
+
files.push({ relativePath: rel, data, contentHash: sha256Hex(data) });
|
|
171
|
+
}
|
|
172
|
+
sections.set(sectionId, { version: 1, files });
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const jsonPath = SECTION_JSON_PATHS[sectionId];
|
|
176
|
+
if (jsonPath === undefined) continue; // secrets 分区无 JSON 文件
|
|
177
|
+
if (!archive.has(jsonPath)) continue; // 声明包含但文件缺失 → 由调用方记 missingSections
|
|
178
|
+
let data: unknown;
|
|
179
|
+
try {
|
|
180
|
+
data = archive.readEntryJson(jsonPath);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
throw new Error(`分区 ${sectionId} 数据解析失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
183
|
+
}
|
|
184
|
+
const issues = validateSectionData(sectionId, data);
|
|
185
|
+
const errors = issues.filter((i) => i.severity === 'error');
|
|
186
|
+
if (errors.length > 0) {
|
|
187
|
+
throw new Error(`分区 ${sectionId} 数据无效: ${errors.map((e) => e.message).join('; ')}`);
|
|
188
|
+
}
|
|
189
|
+
sections.set(sectionId, data);
|
|
190
|
+
}
|
|
191
|
+
return sections;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private async analyzeBundle(bundle: Bundle): Promise<AnalyzedBundle> {
|
|
195
|
+
const sections = this.extractSections(bundle);
|
|
196
|
+
const { manifest } = bundle;
|
|
197
|
+
|
|
198
|
+
const importCtx: ImportContext = {
|
|
199
|
+
manifest,
|
|
200
|
+
targetPlatform: this.ctx.platform,
|
|
201
|
+
target: this.ctx,
|
|
202
|
+
sections,
|
|
203
|
+
pathMappings: [],
|
|
204
|
+
resolutions: {},
|
|
205
|
+
secretInputs: {},
|
|
206
|
+
log: this.ctx.log,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const adapterItems: PlanItem[] = [];
|
|
210
|
+
const adapterIssues: string[] = [];
|
|
211
|
+
for (const adapter of this.adapters) {
|
|
212
|
+
const data = sections.get(adapter.id);
|
|
213
|
+
if (data === undefined) continue;
|
|
214
|
+
try {
|
|
215
|
+
const v = await adapter.validate(data);
|
|
216
|
+
for (const issue of v.issues) {
|
|
217
|
+
if (issue.severity === 'error') adapterIssues.push(`${adapter.id}: ${issue.message}`);
|
|
218
|
+
}
|
|
219
|
+
if (!v.valid) continue;
|
|
220
|
+
const items = await adapter.analyzeImport(data, importCtx);
|
|
221
|
+
adapterItems.push(...items);
|
|
222
|
+
} catch (err) {
|
|
223
|
+
adapterIssues.push(`${adapter.id}: 分析失败 ${err instanceof Error ? err.message : String(err)}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return { ...bundle, sections, adapterItems, adapterIssues };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/* ---------------- 第 8 步:analyzeImport ---------------- */
|
|
231
|
+
|
|
232
|
+
async analyzeImport(zipPath: string): Promise<ImportAnalysis> {
|
|
233
|
+
const bundle = await this.loadBundle(zipPath);
|
|
234
|
+
const { manifest, zipWarnings } = bundle;
|
|
235
|
+
const analyzed = await this.analyzeBundle(bundle);
|
|
236
|
+
|
|
237
|
+
const errors = [...analyzed.adapterIssues];
|
|
238
|
+
const warnings = [...zipWarnings];
|
|
239
|
+
if (!canImport(manifest.schemaVersion)) {
|
|
240
|
+
errors.push(`${describeVersion(manifest.schemaVersion)}:无法导入`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 兼容性(第 6 步)
|
|
244
|
+
const sectionsInZip = [...analyzed.sections.keys()];
|
|
245
|
+
const missingSections = (Object.entries(manifest.sections) as [SectionId, boolean][])
|
|
246
|
+
.filter(([id, on]) => on && !analyzed.sections.has(id))
|
|
247
|
+
.map(([id]) => id);
|
|
248
|
+
if (missingSections.length > 0) {
|
|
249
|
+
warnings.push(`备份声明了但缺少的分区: ${missingSections.join(', ')}`);
|
|
250
|
+
}
|
|
251
|
+
const compatibility = computeCompatibility({
|
|
252
|
+
sourceDsh: manifest.source.dshVersion,
|
|
253
|
+
targetDsh: this.ctx.dshVersion,
|
|
254
|
+
sourcePlatform: manifest.source.platform,
|
|
255
|
+
targetPlatform: this.ctx.platform,
|
|
256
|
+
schemaVersion: manifest.schemaVersion,
|
|
257
|
+
missingSections,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// 路径问题(第 12 步检测;核心只做形态判定,最终映射由 UI 确认)
|
|
261
|
+
const pathIssues = detectPathIssues(
|
|
262
|
+
manifest.source.platform,
|
|
263
|
+
this.ctx.platform,
|
|
264
|
+
analyzed.sections,
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
// 依赖检测(§15:缺失不阻塞,标记 Requires Attention)
|
|
268
|
+
const dependencyIssues: ImportAnalysis['dependencyIssues'] = [];
|
|
269
|
+
if (this.dependencyChecker) {
|
|
270
|
+
const mcp = analyzed.sections.get('mcp') as { servers?: { serverName?: string; command?: string }[] } | undefined;
|
|
271
|
+
for (const server of mcp?.servers ?? []) {
|
|
272
|
+
const cmd = (server.command ?? '').trim();
|
|
273
|
+
if (cmd === '') continue;
|
|
274
|
+
const base = cmd.split(/[\\/ ]/).pop() ?? cmd;
|
|
275
|
+
if (!KNOWN_DEPENDENCIES.has(base)) continue;
|
|
276
|
+
try {
|
|
277
|
+
const ok = await this.dependencyChecker(base);
|
|
278
|
+
if (!ok) dependencyIssues.push({ item: server.serverName ?? base, dependency: base });
|
|
279
|
+
} catch {
|
|
280
|
+
// 检查器异常视为未知,不误报
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 秘密计数:普通备份中已配置(有值)但未导出的凭据数
|
|
286
|
+
const creds = analyzed.sections.get('credentialsStatus') as { credentials?: { configured?: boolean; hasValue?: boolean }[] } | undefined;
|
|
287
|
+
const secretCount = (creds?.credentials ?? []).filter((c) => c.configured === true && c.hasValue !== true).length;
|
|
288
|
+
|
|
289
|
+
// 插件摘要
|
|
290
|
+
const plugins = analyzed.sections.get('plugins') as { plugins?: unknown[] } | undefined;
|
|
291
|
+
const toInstall = analyzed.adapterItems.filter((i) => i.kind === 'Install').length;
|
|
292
|
+
const pluginSummary = {
|
|
293
|
+
installed: Math.max((plugins?.plugins?.length ?? 0) - toInstall, 0),
|
|
294
|
+
toInstall,
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
valid: errors.length === 0,
|
|
299
|
+
errors,
|
|
300
|
+
warnings,
|
|
301
|
+
compatibility,
|
|
302
|
+
sectionsInZip,
|
|
303
|
+
pluginSummary,
|
|
304
|
+
pathIssues,
|
|
305
|
+
secretCount,
|
|
306
|
+
dependencyIssues,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/* ---------------- 第 9 步:createImportPlan(Dry Run 复用) ---------------- */
|
|
311
|
+
|
|
312
|
+
async createImportPlan(zipPath: string, decisions: ImportDecisions): Promise<ImportPlan> {
|
|
313
|
+
const bundle = await this.loadBundle(zipPath);
|
|
314
|
+
const analyzed = await this.analyzeBundle(bundle);
|
|
315
|
+
const { manifest } = bundle;
|
|
316
|
+
|
|
317
|
+
// 先把用户路径映射应用到各分区数据(PathMapper 先行:applyItem 拿到的已是映射后数据)
|
|
318
|
+
applyMappingsToSections(analyzed.sections, decisions.pathMappings);
|
|
319
|
+
|
|
320
|
+
const importCtx: ImportContext = {
|
|
321
|
+
manifest,
|
|
322
|
+
targetPlatform: this.ctx.platform,
|
|
323
|
+
target: this.ctx,
|
|
324
|
+
sections: analyzed.sections,
|
|
325
|
+
pathMappings: decisions.pathMappings,
|
|
326
|
+
resolutions: decisions.resolutions,
|
|
327
|
+
secretInputs: {},
|
|
328
|
+
log: this.ctx.log,
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const items = analyzed.adapterItems.map((item) => applyItemResolution(item, decisions));
|
|
332
|
+
const planMappings = mergePathMappings(items, decisions.pathMappings);
|
|
333
|
+
|
|
334
|
+
// MissingSecret:兜底——credentialsStatus 分区里已配置的凭据若没有对应计划项,补占位
|
|
335
|
+
ensureMissingSecrets(items, analyzed.sections);
|
|
336
|
+
|
|
337
|
+
const missingSecrets = items
|
|
338
|
+
.filter((i) => i.kind === 'MissingSecret')
|
|
339
|
+
.map((i) => ({ ref: i.id.replace(/^secret:/, ''), required: true }));
|
|
340
|
+
|
|
341
|
+
const needsRestart = items.some((i) => i.kind === 'Install' || (i.adapter === 'mcp' && i.kind !== 'Skip' && i.kind !== 'Warning'));
|
|
342
|
+
|
|
343
|
+
const estimatedActions = {} as ImportPlan['estimatedActions'];
|
|
344
|
+
for (const item of items) {
|
|
345
|
+
if (item.kind === 'Skip' || item.kind === 'Warning') continue;
|
|
346
|
+
estimatedActions[item.adapter] = (estimatedActions[item.adapter] ?? 0) + 1;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
items,
|
|
351
|
+
globalStrategy: decisions.strategy,
|
|
352
|
+
pathMappings: planMappings,
|
|
353
|
+
missingSecrets,
|
|
354
|
+
needsRestart,
|
|
355
|
+
estimatedActions,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/* ---------------- 第 11-14 步:executeImportPlan(快照 → 执行 → 校验 → 结果/回滚) ---------------- */
|
|
360
|
+
|
|
361
|
+
async executeImportPlan(
|
|
362
|
+
zipPath: string,
|
|
363
|
+
plan: ImportPlan,
|
|
364
|
+
opts: { confirm?: boolean; secretInputs?: Record<string, string>; decryptedCredentials?: Map<string, string>; rollbackOnError?: boolean } = {},
|
|
365
|
+
): Promise<ImportResult> {
|
|
366
|
+
const bundle = await this.loadBundle(zipPath);
|
|
367
|
+
const analyzed = await this.analyzeBundle(bundle);
|
|
368
|
+
applyMappingsToSections(analyzed.sections, plan.pathMappings);
|
|
369
|
+
|
|
370
|
+
// 10. 用户确认(安全阀:不确认绝不动数据)
|
|
371
|
+
if (opts.confirm !== true) {
|
|
372
|
+
throw new ImportNotConfirmedError();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const importCtx: ImportContext = {
|
|
376
|
+
manifest: bundle.manifest,
|
|
377
|
+
targetPlatform: this.ctx.platform,
|
|
378
|
+
target: this.ctx,
|
|
379
|
+
sections: analyzed.sections,
|
|
380
|
+
pathMappings: plan.pathMappings,
|
|
381
|
+
resolutions: {},
|
|
382
|
+
secretInputs: opts.secretInputs ?? {},
|
|
383
|
+
decryptedCredentials: opts.decryptedCredentials,
|
|
384
|
+
log: this.ctx.log,
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
// 11. 快照(强制:导入前必须先备份将被修改的目标)
|
|
388
|
+
const snapshot = await createSnapshot({
|
|
389
|
+
ctx: this.ctx,
|
|
390
|
+
plan,
|
|
391
|
+
sourceZip: zipPath,
|
|
392
|
+
store: this.snapshotStore,
|
|
393
|
+
adapters: this.adapters,
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
const executed: ExecutedItem[] = [];
|
|
397
|
+
const warnings: string[] = [...bundle.zipWarnings];
|
|
398
|
+
let needsRestart = plan.needsRestart;
|
|
399
|
+
let anyFailed = false;
|
|
400
|
+
|
|
401
|
+
// 12. 分阶段执行
|
|
402
|
+
const byAdapter = new Map<SectionId, PlanItem[]>();
|
|
403
|
+
for (const item of plan.items) {
|
|
404
|
+
const list = byAdapter.get(item.adapter) ?? [];
|
|
405
|
+
list.push(item);
|
|
406
|
+
byAdapter.set(item.adapter, list);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
for (const adapterId of APPLY_ORDER) {
|
|
410
|
+
const adapter = this.adapters.find((a) => a.id === adapterId);
|
|
411
|
+
if (!adapter) continue;
|
|
412
|
+
for (const item of byAdapter.get(adapterId) ?? []) {
|
|
413
|
+
const outcome = await this.applyOne(adapter, item, importCtx);
|
|
414
|
+
executed.push(outcome.executed);
|
|
415
|
+
if (outcome.executed.status === 'failed') anyFailed = true;
|
|
416
|
+
if (outcome.needsRestart) needsRestart = true;
|
|
417
|
+
if (outcome.warning) warnings.push(outcome.warning);
|
|
418
|
+
if (opts.rollbackOnError && outcome.executed.status === 'failed') break;
|
|
419
|
+
}
|
|
420
|
+
if (opts.rollbackOnError && anyFailed) break;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// 失败整体回滚(rollbackOnError):逆序补偿 + 诚实报告
|
|
424
|
+
if (opts.rollbackOnError && anyFailed) {
|
|
425
|
+
const rollbackReport = await rollback({
|
|
426
|
+
ctx: this.ctx,
|
|
427
|
+
snapshot,
|
|
428
|
+
store: this.snapshotStore,
|
|
429
|
+
adapters: this.adapters,
|
|
430
|
+
});
|
|
431
|
+
this.ctx.log.warn(`导入失败,已回滚(${rollbackReport.full ? '完整' : '部分'})`, {
|
|
432
|
+
failed: executed.filter((e) => e.status === 'failed').map((e) => e.itemId),
|
|
433
|
+
});
|
|
434
|
+
return {
|
|
435
|
+
ok: false,
|
|
436
|
+
executed,
|
|
437
|
+
needsRestart: false,
|
|
438
|
+
missingSecrets: [],
|
|
439
|
+
warnings,
|
|
440
|
+
rollback: rollbackReport,
|
|
441
|
+
snapshotId: snapshot.id,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// 13. 校验(执行后:对最终数据再 validate;失败仅告警,不掩盖已完成项)
|
|
446
|
+
for (const adapter of this.adapters) {
|
|
447
|
+
const data = analyzed.sections.get(adapter.id);
|
|
448
|
+
if (data === undefined) continue;
|
|
449
|
+
try {
|
|
450
|
+
const v = await adapter.validate(data);
|
|
451
|
+
for (const issue of v.issues) {
|
|
452
|
+
if (issue.severity === 'error') warnings.push(`导入后校验 ${adapter.id}: ${issue.message}`);
|
|
453
|
+
}
|
|
454
|
+
} catch (err) {
|
|
455
|
+
warnings.push(`导入后校验 ${adapter.id} 失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// 14. 结果
|
|
460
|
+
const missingSecrets = plan.missingSecrets
|
|
461
|
+
.filter((s) => !importCtx.decryptedCredentials?.has(s.ref) && !importCtx.secretInputs[s.ref])
|
|
462
|
+
.map((s) => s.ref);
|
|
463
|
+
|
|
464
|
+
return {
|
|
465
|
+
ok: true, // 单项失败已如实记录在 executed;无未捕获异常即完成
|
|
466
|
+
executed,
|
|
467
|
+
needsRestart,
|
|
468
|
+
missingSecrets,
|
|
469
|
+
warnings,
|
|
470
|
+
rollback: null,
|
|
471
|
+
snapshotId: snapshot.id,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
private async applyOne(
|
|
476
|
+
adapter: ConfigAdapter,
|
|
477
|
+
item: PlanItem,
|
|
478
|
+
ctx: ImportContext,
|
|
479
|
+
): Promise<{ executed: ExecutedItem; needsRestart: boolean; warning?: string }> {
|
|
480
|
+
if (item.kind === 'Skip' || (item.kind === 'Conflict' && item.conflict?.resolution !== 'useImported')) {
|
|
481
|
+
return { executed: { itemId: item.id, status: 'skipped' }, needsRestart: false };
|
|
482
|
+
}
|
|
483
|
+
if (item.kind === 'Error') {
|
|
484
|
+
return { executed: { itemId: item.id, status: 'failed', message: item.detail ?? item.description }, needsRestart: false };
|
|
485
|
+
}
|
|
486
|
+
if (item.kind === 'MissingSecret') {
|
|
487
|
+
const ref = item.id.replace(/^secret:/, '');
|
|
488
|
+
const value = ctx.decryptedCredentials?.get(ref) ?? ctx.secretInputs[ref];
|
|
489
|
+
if (value === undefined || value === '') {
|
|
490
|
+
return { executed: { itemId: item.id, status: 'skipped', message: '凭据未提供,需补录' }, needsRestart: false };
|
|
491
|
+
}
|
|
492
|
+
// 补录值只经 adapter.applyItem 写入(m5 实现),引擎不直接触碰凭据
|
|
493
|
+
}
|
|
494
|
+
try {
|
|
495
|
+
const result: ApplyResult = await adapter.applyItem(item, ctx);
|
|
496
|
+
return {
|
|
497
|
+
executed: { itemId: item.id, status: result.ok ? 'ok' : 'failed', message: result.message },
|
|
498
|
+
needsRestart: result.needsRestart === true,
|
|
499
|
+
};
|
|
500
|
+
} catch (err) {
|
|
501
|
+
this.ctx.log.error(`应用计划项失败 ${item.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
502
|
+
return {
|
|
503
|
+
executed: { itemId: item.id, status: 'failed', message: err instanceof Error ? err.message : String(err) },
|
|
504
|
+
needsRestart: false,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/* ---------------- 纯函数辅助 ---------------- */
|
|
511
|
+
|
|
512
|
+
async function verifyAgainstTable(
|
|
513
|
+
entries: Map<string, Uint8Array>,
|
|
514
|
+
table: Record<string, string>,
|
|
515
|
+
): Promise<{ ok: boolean; mismatches: string[]; missing: string[] }> {
|
|
516
|
+
const mismatches: string[] = [];
|
|
517
|
+
const missing: string[] = [];
|
|
518
|
+
for (const [relPath, expected] of Object.entries(table)) {
|
|
519
|
+
const data = entries.get(relPath);
|
|
520
|
+
if (data === undefined) {
|
|
521
|
+
missing.push(relPath);
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
if (sha256Hex(data) !== expected) mismatches.push(relPath);
|
|
525
|
+
}
|
|
526
|
+
return { ok: mismatches.length === 0 && missing.length === 0, mismatches, missing };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/** 应用用户冲突决策 + 全局策略(纯函数,返回新数组) */
|
|
530
|
+
function applyItemResolution(item: PlanItem, decisions: ImportDecisions): PlanItem {
|
|
531
|
+
if (item.kind !== 'Conflict') return item;
|
|
532
|
+
const resolution = decisions.resolutions[item.id];
|
|
533
|
+
if (resolution === 'keepCurrent') {
|
|
534
|
+
return { ...item, kind: 'Skip', severity: 'info', detail: `${item.detail ?? ''}(用户选择保留当前)` };
|
|
535
|
+
}
|
|
536
|
+
if (resolution === 'useImported') {
|
|
537
|
+
return { ...item, kind: 'Update', severity: 'info', conflict: { itemId: item.id, resolution } };
|
|
538
|
+
}
|
|
539
|
+
// review / 未决策:按全局策略兜底
|
|
540
|
+
if (decisions.strategy === 'skipExisting') {
|
|
541
|
+
return { ...item, kind: 'Skip', severity: 'info', detail: `${item.detail ?? ''}(skipExisting 策略)` };
|
|
542
|
+
}
|
|
543
|
+
if (decisions.strategy === 'replace') {
|
|
544
|
+
return { ...item, kind: 'Update', severity: 'info', detail: `${item.detail ?? ''}(replace 策略)` };
|
|
545
|
+
}
|
|
546
|
+
return item; // merge + 未决策 → 保持 Conflict,由报告列明
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** 路径映射合并:只保留「已解析」(newPrefix 非空)的映射用于执行期数据改写;
|
|
550
|
+
* 未解决项(newPrefix 空)保留在 plan.items 里供 UI 提示,但不参与数据映射。 */
|
|
551
|
+
function mergePathMappings(items: PlanItem[], userMappings: PathMapping[]): PathMapping[] {
|
|
552
|
+
const merged = new Map<string, PathMapping>();
|
|
553
|
+
for (const m of userMappings) {
|
|
554
|
+
if (m.newPrefix !== '') merged.set(m.oldPrefix, m);
|
|
555
|
+
}
|
|
556
|
+
return [...merged.values()];
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/** 把映射应用到分区数据(PathMapper 先行:只替换匹配前缀的字符串叶值) */
|
|
560
|
+
function applyMappingsToSections(sections: Map<SectionId, unknown>, mappings: PathMapping[]): void {
|
|
561
|
+
if (mappings.length === 0) return;
|
|
562
|
+
for (const [sectionId, data] of sections) {
|
|
563
|
+
const appliesTo: PathMapping['appliesTo'] =
|
|
564
|
+
sectionId === 'workspaces' ? ['workspaces']
|
|
565
|
+
: sectionId === 'mcp' ? ['mcp']
|
|
566
|
+
: sectionId === 'skills' ? ['skills']
|
|
567
|
+
: sectionId === 'pluginFiles' ? ['pluginConfig']
|
|
568
|
+
: [];
|
|
569
|
+
const relevant = mappings.filter((m) => m.appliesTo.some((a) => appliesTo.includes(a as never)) || m.appliesTo.length === 0);
|
|
570
|
+
if (relevant.length === 0) continue;
|
|
571
|
+
sections.set(sectionId, applyPrefixMappings(data, relevant));
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/** 路径形态判定:跨平台盘符/UNIX 冲突 → platformMismatch;其余绝对路径 → missing(需映射) */
|
|
576
|
+
function detectPathIssues(
|
|
577
|
+
sourcePlatform: string,
|
|
578
|
+
targetPlatform: string,
|
|
579
|
+
sections: Map<SectionId, unknown>,
|
|
580
|
+
): PathIssue[] {
|
|
581
|
+
const issues: PathIssue[] = [];
|
|
582
|
+
const workspaces = sections.get('workspaces') as { workspaces?: { path?: string }[] } | undefined;
|
|
583
|
+
for (const w of workspaces?.workspaces ?? []) {
|
|
584
|
+
const p = w.path;
|
|
585
|
+
if (p && isAbsolutePath(p)) issues.push(judgePath(p, sourcePlatform, targetPlatform));
|
|
586
|
+
}
|
|
587
|
+
const mcp = sections.get('mcp') as { servers?: { serverName?: string; command?: string; cwd?: string }[] } | undefined;
|
|
588
|
+
for (const s of mcp?.servers ?? []) {
|
|
589
|
+
for (const p of [s.cwd, s.command]) {
|
|
590
|
+
if (p && isAbsolutePath(p)) issues.push(judgePath(p, sourcePlatform, targetPlatform));
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
// 去重
|
|
594
|
+
const seen = new Set<string>();
|
|
595
|
+
return issues.filter((i) => {
|
|
596
|
+
const key = `${i.kind}:${i.value}`;
|
|
597
|
+
if (seen.has(key)) return false;
|
|
598
|
+
seen.add(key);
|
|
599
|
+
return true;
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function judgePath(p: string, sourcePlatform: string, targetPlatform: string): PathIssue {
|
|
604
|
+
const isWinStyle = /^[a-zA-Z]:[\\/]/.test(p);
|
|
605
|
+
const isUnixStyle = p.startsWith('/');
|
|
606
|
+
if (sourcePlatform !== targetPlatform && ((isWinStyle && targetPlatform !== 'win32') || (isUnixStyle && targetPlatform === 'win32'))) {
|
|
607
|
+
return { kind: 'platformMismatch', value: p };
|
|
608
|
+
}
|
|
609
|
+
return { kind: 'missing', value: p };
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/** MissingSecret 兜底:credentialsStatus 分区已配置凭据若无对应计划项,补占位 */
|
|
613
|
+
function ensureMissingSecrets(items: PlanItem[], sections: Map<SectionId, unknown>): void {
|
|
614
|
+
const creds = sections.get('credentialsStatus') as { credentials?: { ref?: string; configured?: boolean }[] } | undefined;
|
|
615
|
+
if (!creds?.credentials) return;
|
|
616
|
+
const existing = new Set(items.filter((i) => i.kind === 'MissingSecret').map((i) => i.id));
|
|
617
|
+
for (const c of creds.credentials) {
|
|
618
|
+
if (!c.ref || c.configured !== true) continue;
|
|
619
|
+
const id = `secret:${c.ref}`;
|
|
620
|
+
if (existing.has(id)) continue;
|
|
621
|
+
items.push({
|
|
622
|
+
id,
|
|
623
|
+
kind: 'MissingSecret',
|
|
624
|
+
adapter: 'credentialsStatus',
|
|
625
|
+
description: `凭据 ${c.ref} 需要补录`,
|
|
626
|
+
severity: 'warning',
|
|
627
|
+
target: { adapter: 'credentialsStatus', ref: c.ref },
|
|
628
|
+
});
|
|
629
|
+
existing.add(id);
|
|
630
|
+
}
|
|
631
|
+
}
|