tanmi-dock 1.0.3 → 1.0.5-beta
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 +18 -0
- package/dist/commands/calibrate.d.ts +10 -0
- package/dist/commands/calibrate.d.ts.map +1 -0
- package/dist/commands/calibrate.js +158 -0
- package/dist/commands/calibrate.js.map +1 -0
- package/dist/commands/check.d.ts +38 -4
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +704 -168
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/clean.d.ts +2 -6
- package/dist/commands/clean.d.ts.map +1 -1
- package/dist/commands/clean.js +224 -209
- package/dist/commands/clean.js.map +1 -1
- package/dist/commands/dashboard.d.ts.map +1 -1
- package/dist/commands/dashboard.js +56 -28
- package/dist/commands/dashboard.js.map +1 -1
- package/dist/commands/link.d.ts +6 -6
- package/dist/commands/link.d.ts.map +1 -1
- package/dist/commands/link.js +417 -196
- package/dist/commands/link.js.map +1 -1
- package/dist/commands/migrate.d.ts +11 -0
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +277 -57
- package/dist/commands/migrate.js.map +1 -1
- package/dist/commands/reset.d.ts.map +1 -1
- package/dist/commands/reset.js +19 -24
- package/dist/commands/reset.js.map +1 -1
- package/dist/commands/status.d.ts +4 -0
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +37 -20
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/unavailable.js.map +1 -1
- package/dist/commands/unlink.d.ts.map +1 -1
- package/dist/commands/unlink.js +43 -39
- package/dist/commands/unlink.js.map +1 -1
- package/dist/core/config.d.ts +3 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +10 -6
- package/dist/core/config.js.map +1 -1
- package/dist/core/parser.js +1 -1
- package/dist/core/parser.js.map +1 -1
- package/dist/core/registry.d.ts +11 -29
- package/dist/core/registry.d.ts.map +1 -1
- package/dist/core/registry.js +46 -113
- package/dist/core/registry.js.map +1 -1
- package/dist/core/space/calibration-executor.d.ts +17 -0
- package/dist/core/space/calibration-executor.d.ts.map +1 -0
- package/dist/core/space/calibration-executor.js +144 -0
- package/dist/core/space/calibration-executor.js.map +1 -0
- package/dist/core/space/calibration-planner.d.ts +5 -0
- package/dist/core/space/calibration-planner.d.ts.map +1 -0
- package/dist/core/space/calibration-planner.js +157 -0
- package/dist/core/space/calibration-planner.js.map +1 -0
- package/dist/core/space/cleanup-executor.d.ts +15 -0
- package/dist/core/space/cleanup-executor.d.ts.map +1 -0
- package/dist/core/space/cleanup-executor.js +188 -0
- package/dist/core/space/cleanup-executor.js.map +1 -0
- package/dist/core/space/cleanup-journal.d.ts +43 -0
- package/dist/core/space/cleanup-journal.d.ts.map +1 -0
- package/dist/core/space/cleanup-journal.js +96 -0
- package/dist/core/space/cleanup-journal.js.map +1 -0
- package/dist/core/space/cleanup-planner.d.ts +14 -0
- package/dist/core/space/cleanup-planner.d.ts.map +1 -0
- package/dist/core/space/cleanup-planner.js +166 -0
- package/dist/core/space/cleanup-planner.js.map +1 -0
- package/dist/core/space/cleanup-selection.d.ts +10 -0
- package/dist/core/space/cleanup-selection.d.ts.map +1 -0
- package/dist/core/space/cleanup-selection.js +84 -0
- package/dist/core/space/cleanup-selection.js.map +1 -0
- package/dist/core/space/estimator.d.ts +11 -0
- package/dist/core/space/estimator.d.ts.map +1 -0
- package/dist/core/space/estimator.js +142 -0
- package/dist/core/space/estimator.js.map +1 -0
- package/dist/core/space/inventory.d.ts +16 -0
- package/dist/core/space/inventory.d.ts.map +1 -0
- package/dist/core/space/inventory.js +610 -0
- package/dist/core/space/inventory.js.map +1 -0
- package/dist/core/space/meter.d.ts +5 -0
- package/dist/core/space/meter.d.ts.map +1 -0
- package/dist/core/space/meter.js +87 -0
- package/dist/core/space/meter.js.map +1 -0
- package/dist/core/space/migration-journal.d.ts +31 -0
- package/dist/core/space/migration-journal.d.ts.map +1 -0
- package/dist/core/space/migration-journal.js +121 -0
- package/dist/core/space/migration-journal.js.map +1 -0
- package/dist/core/space/service.d.ts +26 -0
- package/dist/core/space/service.d.ts.map +1 -0
- package/dist/core/space/service.js +54 -0
- package/dist/core/space/service.js.map +1 -0
- package/dist/core/space/types.d.ts +248 -0
- package/dist/core/space/types.d.ts.map +1 -0
- package/dist/core/space/types.js +13 -0
- package/dist/core/space/types.js.map +1 -0
- package/dist/core/store.d.ts.map +1 -1
- package/dist/core/store.js +12 -6
- package/dist/core/store.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/atomic-file.d.ts +6 -0
- package/dist/utils/atomic-file.d.ts.map +1 -0
- package/dist/utils/atomic-file.js +37 -0
- package/dist/utils/atomic-file.js.map +1 -0
- package/dist/utils/fs-utils.d.ts +18 -1
- package/dist/utils/fs-utils.d.ts.map +1 -1
- package/dist/utils/fs-utils.js +150 -2
- package/dist/utils/fs-utils.js.map +1 -1
- package/dist/utils/logger.d.ts +7 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +45 -5
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/prompt.d.ts.map +1 -1
- package/dist/utils/prompt.js +4 -4
- package/dist/utils/prompt.js.map +1 -1
- package/package.json +1 -1
package/dist/commands/check.js
CHANGED
|
@@ -9,14 +9,91 @@ import { getDiskInfo, formatSize } from '../utils/disk.js';
|
|
|
9
9
|
import * as config from '../core/config.js';
|
|
10
10
|
import { getRegistry } from '../core/registry.js';
|
|
11
11
|
import * as store from '../core/store.js';
|
|
12
|
-
import { KNOWN_PLATFORM_VALUES } from '../core/platform.js';
|
|
13
|
-
import {
|
|
12
|
+
import { GENERAL_PLATFORM, KNOWN_PLATFORM_VALUES, SHARED_PLATFORM } from '../core/platform.js';
|
|
13
|
+
import { SpaceService } from '../core/space/service.js';
|
|
14
|
+
import { CleanupJournal } from '../core/space/cleanup-journal.js';
|
|
15
|
+
import { MigrationJournal } from '../core/space/migration-journal.js';
|
|
16
|
+
import { withGlobalLock } from '../utils/global-lock.js';
|
|
17
|
+
import { info, warn, success, error, hint, blank, title, separator, colorize, debug, withStdoutLogsSuppressed, } from '../utils/logger.js';
|
|
14
18
|
import { selectWithCancel, checkboxWithCancel, confirmWithCancel, PROMPT_CANCELLED, } from '../utils/prompt.js';
|
|
19
|
+
function createSpaceProgressRenderer(label) {
|
|
20
|
+
let lineWidth = 0;
|
|
21
|
+
let lastReported = -1;
|
|
22
|
+
const close = () => {
|
|
23
|
+
if (lineWidth > 0 && process.stdout.isTTY) {
|
|
24
|
+
process.stdout.write(`\r${' '.repeat(lineWidth)}\r`);
|
|
25
|
+
}
|
|
26
|
+
lineWidth = 0;
|
|
27
|
+
};
|
|
28
|
+
return {
|
|
29
|
+
update(progress) {
|
|
30
|
+
if (progress.phase === 'completed') {
|
|
31
|
+
close();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const message = progress.phase === 'measure-store'
|
|
35
|
+
? `${label}: 正在汇总 Store 总占用...`
|
|
36
|
+
: `${label}: ${progress.completedCommits}/${progress.totalCommits} 个版本`;
|
|
37
|
+
if (process.stdout.isTTY) {
|
|
38
|
+
const padded = message.padEnd(Math.max(lineWidth, message.length));
|
|
39
|
+
lineWidth = padded.length;
|
|
40
|
+
process.stdout.write(`\r${padded}`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const interval = Math.max(1, Math.ceil(progress.totalCommits / 10));
|
|
44
|
+
const shouldReport = progress.phase === 'measure-store' ||
|
|
45
|
+
progress.completedCommits === 0 ||
|
|
46
|
+
progress.completedCommits === progress.totalCommits ||
|
|
47
|
+
progress.completedCommits - lastReported >= interval;
|
|
48
|
+
if (shouldReport && progress.completedCommits !== lastReported) {
|
|
49
|
+
info(message);
|
|
50
|
+
lastReported = progress.completedCommits;
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
close,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function createCheckProgressRenderer() {
|
|
57
|
+
const spaceProgress = createSpaceProgressRenderer('[3/3] 正在审计 Store 空间');
|
|
58
|
+
return (progress) => {
|
|
59
|
+
if (progress.stage === 'space' && progress.state === 'progress') {
|
|
60
|
+
spaceProgress.update(progress.progress);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (progress.stage === 'space')
|
|
64
|
+
spaceProgress.close();
|
|
65
|
+
const indexes = { environment: 1, integrity: 2, space: 3 };
|
|
66
|
+
if (progress.state === 'started') {
|
|
67
|
+
const messages = {
|
|
68
|
+
environment: '正在检查运行环境...',
|
|
69
|
+
integrity: '正在核对项目与依赖...',
|
|
70
|
+
space: '正在审计 Store 空间...',
|
|
71
|
+
};
|
|
72
|
+
info(`[${indexes[progress.stage]}/3] ${messages[progress.stage]}`);
|
|
73
|
+
}
|
|
74
|
+
else if (progress.state === 'completed') {
|
|
75
|
+
const messages = {
|
|
76
|
+
environment: '运行环境检查完成',
|
|
77
|
+
integrity: '项目与依赖核对完成',
|
|
78
|
+
space: 'Store 空间审计完成',
|
|
79
|
+
};
|
|
80
|
+
info(`[${indexes[progress.stage]}/3] ${messages[progress.stage]}`);
|
|
81
|
+
}
|
|
82
|
+
else if (progress.state === 'failed') {
|
|
83
|
+
const labels = {
|
|
84
|
+
environment: '运行环境',
|
|
85
|
+
integrity: '项目与依赖',
|
|
86
|
+
space: 'Store 空间',
|
|
87
|
+
};
|
|
88
|
+
warn(`[${indexes[progress.stage]}/3] ${labels[progress.stage]}检查失败: ${progress.error ?? '未知错误'}`);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
15
92
|
// ============ 命令创建 ============
|
|
16
93
|
export function createCheckCommand() {
|
|
17
94
|
return new Command('check')
|
|
18
95
|
.description('健康检查(环境诊断 + 数据一致性验证)')
|
|
19
|
-
.option('--fix', '
|
|
96
|
+
.option('--fix', '直接修复所有可修复问题')
|
|
20
97
|
.option('--dry-run', '只显示问题,不修复')
|
|
21
98
|
.option('--json', '输出 JSON 格式')
|
|
22
99
|
.option('--force', '跳过确认')
|
|
@@ -27,7 +104,26 @@ export function createCheckCommand() {
|
|
|
27
104
|
}
|
|
28
105
|
// ============ 主流程 ============
|
|
29
106
|
async function runCheck(options) {
|
|
30
|
-
|
|
107
|
+
if (options.json) {
|
|
108
|
+
await withStdoutLogsSuppressed(() => runCheckInternal(options));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
await runCheckInternal(options);
|
|
112
|
+
}
|
|
113
|
+
async function runCheckInternal(options) {
|
|
114
|
+
debug(`[check] 命令输入: fix=${options.fix}, dryRun=${options.dryRun}, json=${options.json}, force=${options.force}, integrity=${options.integrity}`);
|
|
115
|
+
if (options.dryRun) {
|
|
116
|
+
const cfg = await config.load({ migrate: false });
|
|
117
|
+
if (cfg && config.checkConfigVersion(cfg) === 'migrate') {
|
|
118
|
+
error('配置需要迁移;dry-run 不会修改配置,请先运行一次普通命令完成迁移');
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const result = await collectAllIssues({
|
|
123
|
+
integrity: options.integrity,
|
|
124
|
+
readOnly: options.dryRun,
|
|
125
|
+
onProgress: options.json ? undefined : createCheckProgressRenderer(),
|
|
126
|
+
});
|
|
31
127
|
// JSON 输出
|
|
32
128
|
if (options.json) {
|
|
33
129
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -35,13 +131,15 @@ async function runCheck(options) {
|
|
|
35
131
|
}
|
|
36
132
|
// 渲染报告
|
|
37
133
|
renderReport(result);
|
|
38
|
-
const
|
|
134
|
+
const hasIntegrityIssues = result.summary.integrityIssues > 0;
|
|
135
|
+
const hasReportedIssues = hasIntegrityIssues || result.summary.spaceIssues > 0;
|
|
136
|
+
const hasRepairableIssues = hasIntegrityIssues || hasSpaceCalibrationChoice(result.space);
|
|
39
137
|
const hasEnvErrors = result.summary.envErrors > 0;
|
|
40
138
|
// dry-run 模式
|
|
41
139
|
if (options.dryRun) {
|
|
42
|
-
if (
|
|
140
|
+
if (hasRepairableIssues) {
|
|
43
141
|
blank();
|
|
44
|
-
hint('运行 td check --fix
|
|
142
|
+
hint('运行 td check --fix 修复可修复问题');
|
|
45
143
|
}
|
|
46
144
|
return;
|
|
47
145
|
}
|
|
@@ -52,21 +150,90 @@ async function runCheck(options) {
|
|
|
52
150
|
return;
|
|
53
151
|
}
|
|
54
152
|
// 无问题
|
|
55
|
-
if (!
|
|
153
|
+
if (!hasRepairableIssues) {
|
|
154
|
+
if (hasReportedIssues) {
|
|
155
|
+
debug('[check] 检查结果只包含无自动修复动作的问题');
|
|
156
|
+
}
|
|
56
157
|
return;
|
|
57
158
|
}
|
|
58
159
|
// --fix 模式:直接修复
|
|
59
160
|
if (options.fix) {
|
|
60
|
-
await fixAllIssues(result.integrity,
|
|
161
|
+
await fixAllIssues(result.integrity, result.space, {
|
|
162
|
+
force: options.force,
|
|
163
|
+
calibrateSpace: hasSpaceCalibrationChoice(result.space),
|
|
164
|
+
deleteUnregistered: false,
|
|
165
|
+
verifyStoreIntegrity: options.integrity,
|
|
166
|
+
});
|
|
167
|
+
renderUncachedDependencyHint(result.integrity);
|
|
61
168
|
return;
|
|
62
169
|
}
|
|
63
170
|
// 交互模式
|
|
64
|
-
await interactiveCheck(result
|
|
171
|
+
await interactiveCheck(result, options);
|
|
65
172
|
}
|
|
66
173
|
// ============ 数据收集 ============
|
|
67
|
-
async function collectAllIssues(options) {
|
|
68
|
-
|
|
174
|
+
async function collectAllIssues(options = {}) {
|
|
175
|
+
debug(`[check] 健康检查开始: integrity=${options.integrity ?? false}, readOnly=${options.readOnly ?? false}, progress=${options.onProgress ? 'enabled' : 'disabled'}`);
|
|
176
|
+
options.onProgress?.({ stage: 'environment', state: 'started' });
|
|
177
|
+
const environment = await checkEnvironment(options?.readOnly);
|
|
178
|
+
options.onProgress?.({ stage: 'environment', state: 'completed' });
|
|
179
|
+
options.onProgress?.({ stage: 'integrity', state: 'started' });
|
|
69
180
|
const integrity = await checkIntegrity(options);
|
|
181
|
+
options.onProgress?.({ stage: 'integrity', state: 'completed' });
|
|
182
|
+
const space = { issues: [] };
|
|
183
|
+
options.onProgress?.({ stage: 'space', state: 'started' });
|
|
184
|
+
try {
|
|
185
|
+
const registry = getRegistry();
|
|
186
|
+
await registry.load({ migrate: !options?.readOnly });
|
|
187
|
+
const storePath = await store.getStorePath();
|
|
188
|
+
debug(`[space] audit 开始: store=${storePath}, source=td-check`);
|
|
189
|
+
const spaceService = new SpaceService(registry, storePath);
|
|
190
|
+
const snapshot = await spaceService.buildSnapshot('audit', undefined, (progress) => options.onProgress?.({ stage: 'space', state: 'progress', progress }));
|
|
191
|
+
space.snapshotId = snapshot.id;
|
|
192
|
+
space.totals = snapshot.totals;
|
|
193
|
+
space.issues = snapshot.issues;
|
|
194
|
+
space.calibrationPlan = spaceService.buildCalibrationPlan(snapshot);
|
|
195
|
+
const cleanupLogs = await CleanupJournal.inspectPending();
|
|
196
|
+
for (const journal of cleanupLogs.journals) {
|
|
197
|
+
const data = journal.getData();
|
|
198
|
+
space.issues.push({
|
|
199
|
+
kind: 'recovery-log',
|
|
200
|
+
id: { libName: 'cleanup-journal', commit: data.planId },
|
|
201
|
+
message: `存在待恢复清理日志,组状态: ${data.groups.map((group) => `${group.index}:${group.state}`).join(', ')}`,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
for (const filePath of cleanupLogs.corruptedFiles) {
|
|
205
|
+
space.issues.push({
|
|
206
|
+
kind: 'recovery-log',
|
|
207
|
+
id: { libName: 'cleanup-journal', commit: path.basename(filePath) },
|
|
208
|
+
message: `清理日志损坏,已保留原文件: ${filePath}`,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
const migrations = await MigrationJournal.listPending();
|
|
213
|
+
for (const migration of migrations) {
|
|
214
|
+
const data = migration.getData();
|
|
215
|
+
space.issues.push({
|
|
216
|
+
kind: 'recovery-log',
|
|
217
|
+
id: { libName: 'migration-journal', commit: data.id },
|
|
218
|
+
message: `存在待恢复迁移: ${data.sourcePath} -> ${data.destinationPath},阶段 ${data.phase}`,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch (migrationError) {
|
|
223
|
+
space.issues.push({
|
|
224
|
+
kind: 'recovery-log',
|
|
225
|
+
id: { libName: 'migration-journal', commit: 'corrupted' },
|
|
226
|
+
message: migrationError.message,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
debug(`[space] audit 完成: snapshot=${snapshot.id}, physical=${snapshot.totals.physical.bytes}, registered=${snapshot.totals.registered.bytes}, difference=${snapshot.totals.differenceBytes}, issues=${space.issues.length}, pendingCleanupLogs=${cleanupLogs.journals.length}, corruptedCleanupLogs=${cleanupLogs.corruptedFiles.length}`);
|
|
230
|
+
options.onProgress?.({ stage: 'space', state: 'completed' });
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
space.error = err.message;
|
|
234
|
+
debug(`[space] audit 失败: error=${space.error}`);
|
|
235
|
+
options.onProgress?.({ stage: 'space', state: 'failed', error: space.error });
|
|
236
|
+
}
|
|
70
237
|
// 计算汇总
|
|
71
238
|
// 错误:检查项失败(ok === false),排除仅作为警告的项
|
|
72
239
|
// 警告:目前只有 disk 有 warn 字段(空间 < 5GB 但不影响功能)
|
|
@@ -80,22 +247,27 @@ async function collectAllIssues(options) {
|
|
|
80
247
|
const integrityIssues = integrity.invalidProjects.length +
|
|
81
248
|
integrity.danglingLinks.length +
|
|
82
249
|
integrity.orphanLibraries.length +
|
|
83
|
-
integrity.missingLibraries.length +
|
|
84
250
|
integrity.staleReferences.length +
|
|
85
251
|
integrity.corruptedStores.length;
|
|
86
|
-
const reclaimableSize =
|
|
87
|
-
|
|
252
|
+
const reclaimableSize = space.totals?.reclaimable.bytes
|
|
253
|
+
?? integrity.orphanLibraries.reduce((sum, lib) => sum + lib.size, 0);
|
|
254
|
+
const result = {
|
|
88
255
|
environment,
|
|
89
256
|
integrity,
|
|
257
|
+
space,
|
|
90
258
|
summary: {
|
|
91
259
|
envErrors,
|
|
92
260
|
envWarnings,
|
|
93
261
|
integrityIssues,
|
|
262
|
+
uncachedDependencies: integrity.missingLibraries.length,
|
|
94
263
|
reclaimableSize,
|
|
264
|
+
spaceIssues: space.issues.length,
|
|
95
265
|
},
|
|
96
266
|
};
|
|
267
|
+
debug(`[check] 健康检查完成: envErrors=${envErrors}, envWarnings=${envWarnings}, integrityIssues=${integrityIssues}, uncachedDependencies=${integrity.missingLibraries.length}, spaceIssues=${space.issues.length}, spaceSnapshot=${space.snapshotId ?? 'none'}, spaceError=${space.error ?? 'none'}`);
|
|
268
|
+
return result;
|
|
97
269
|
}
|
|
98
|
-
async function checkEnvironment() {
|
|
270
|
+
async function checkEnvironment(readOnly = false) {
|
|
99
271
|
const codepacEnvironment = await checkCodepacEnvironment();
|
|
100
272
|
const result = {
|
|
101
273
|
codepac: { ok: false, message: '', details: codepacEnvironment },
|
|
@@ -111,7 +283,7 @@ async function checkEnvironment() {
|
|
|
111
283
|
details: codepacEnvironment,
|
|
112
284
|
};
|
|
113
285
|
// 2. 配置
|
|
114
|
-
const cfg = await config.load();
|
|
286
|
+
const cfg = await config.load({ migrate: !readOnly });
|
|
115
287
|
result.config = {
|
|
116
288
|
ok: !!cfg,
|
|
117
289
|
message: cfg ? '已初始化' : '未初始化,运行 td init',
|
|
@@ -179,7 +351,7 @@ async function checkIntegrity(options) {
|
|
|
179
351
|
let storePath;
|
|
180
352
|
try {
|
|
181
353
|
registry = getRegistry();
|
|
182
|
-
await registry.load();
|
|
354
|
+
await registry.load({ migrate: !options?.readOnly });
|
|
183
355
|
storePath = await store.getStorePath();
|
|
184
356
|
}
|
|
185
357
|
catch (err) {
|
|
@@ -188,6 +360,7 @@ async function checkIntegrity(options) {
|
|
|
188
360
|
return result;
|
|
189
361
|
}
|
|
190
362
|
const projects = registry.listProjects();
|
|
363
|
+
debug(`[check] 数据一致性检查开始: store=${storePath}, projects=${projects.length}, integrity=${options?.integrity ?? false}, readOnly=${options?.readOnly ?? false}`);
|
|
191
364
|
// 1. 检查项目和链接
|
|
192
365
|
for (const project of projects) {
|
|
193
366
|
const projectHash = registry.hashPath(project.path);
|
|
@@ -207,6 +380,25 @@ async function checkIntegrity(options) {
|
|
|
207
380
|
if (!verifyPlatform) {
|
|
208
381
|
continue;
|
|
209
382
|
}
|
|
383
|
+
const libraryKey = registry.getLibraryKey(dep.libName, dep.commit);
|
|
384
|
+
const library = registry.getLibrary(libraryKey);
|
|
385
|
+
const hasGeneralStoreEntry = registry
|
|
386
|
+
.getLibraryStoreKeys(dep.libName, dep.commit)
|
|
387
|
+
.some((storeKey) => registry.getStore(storeKey)?.platform === GENERAL_PLATFORM);
|
|
388
|
+
const isGeneral = library?.isGeneral === true ||
|
|
389
|
+
library?.platforms.includes(GENERAL_PLATFORM) === true ||
|
|
390
|
+
hasGeneralStoreEntry;
|
|
391
|
+
const cachePlatform = isGeneral ? GENERAL_PLATFORM : verifyPlatform;
|
|
392
|
+
const cacheExists = await store.exists(dep.libName, dep.commit, cachePlatform);
|
|
393
|
+
if (!cacheExists) {
|
|
394
|
+
result.missingLibraries.push({
|
|
395
|
+
libName: dep.libName,
|
|
396
|
+
commit: dep.commit,
|
|
397
|
+
project: project.path,
|
|
398
|
+
});
|
|
399
|
+
debug(`[check] 本地依赖缓存缺失: project=${project.path}, lib=${dep.libName}, commit=${dep.commit}, platform=${cachePlatform}, link=${linkPath}`);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
210
402
|
try {
|
|
211
403
|
const stat = await fs.lstat(linkPath);
|
|
212
404
|
if (stat.isSymbolicLink()) {
|
|
@@ -219,21 +411,19 @@ async function checkIntegrity(options) {
|
|
|
219
411
|
result.danglingLinks.push({
|
|
220
412
|
path: linkPath,
|
|
221
413
|
projectHash,
|
|
222
|
-
dep: {
|
|
414
|
+
dep: {
|
|
415
|
+
libName: dep.libName,
|
|
416
|
+
commit: dep.commit,
|
|
417
|
+
platform: dep.platform,
|
|
418
|
+
linkedPath: dep.linkedPath,
|
|
419
|
+
...(dep.scope ? { scope: dep.scope } : {}),
|
|
420
|
+
},
|
|
223
421
|
});
|
|
224
422
|
}
|
|
225
423
|
}
|
|
226
424
|
}
|
|
227
425
|
catch {
|
|
228
|
-
//
|
|
229
|
-
const exists = await store.exists(dep.libName, dep.commit, verifyPlatform);
|
|
230
|
-
if (!exists) {
|
|
231
|
-
result.missingLibraries.push({
|
|
232
|
-
libName: dep.libName,
|
|
233
|
-
commit: dep.commit,
|
|
234
|
-
project: project.path,
|
|
235
|
-
});
|
|
236
|
-
}
|
|
426
|
+
// Store 缓存存在但项目链接缺失,后续引用检查会报告失效引用。
|
|
237
427
|
}
|
|
238
428
|
}
|
|
239
429
|
}
|
|
@@ -348,7 +538,7 @@ async function checkIntegrity(options) {
|
|
|
348
538
|
if (options?.integrity) {
|
|
349
539
|
const stores = registry.listStores();
|
|
350
540
|
for (const entry of stores) {
|
|
351
|
-
if (entry.fileCount != null) {
|
|
541
|
+
if (entry.fileCount != null && entry.contentHash) {
|
|
352
542
|
// 有 fileCount 的做 fullVerify
|
|
353
543
|
const expectedData = {
|
|
354
544
|
size: entry.size,
|
|
@@ -368,24 +558,19 @@ async function checkIntegrity(options) {
|
|
|
368
558
|
}
|
|
369
559
|
}
|
|
370
560
|
else {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
}
|
|
381
|
-
catch {
|
|
382
|
-
debug(`[check] 回填完整性数据失败: ${entry.libName}:${entry.commit}:${entry.platform}`);
|
|
383
|
-
}
|
|
561
|
+
result.corruptedStores.push({
|
|
562
|
+
libName: entry.libName,
|
|
563
|
+
commit: entry.commit,
|
|
564
|
+
platform: entry.platform,
|
|
565
|
+
kind: 'missing_integrity_record',
|
|
566
|
+
reason: 'missing integrity record',
|
|
567
|
+
expected: { size: entry.size },
|
|
568
|
+
actual: { fileCount: 0, size: 0 },
|
|
569
|
+
});
|
|
384
570
|
}
|
|
385
571
|
}
|
|
386
|
-
// 保存回填的数据
|
|
387
|
-
await registry.save();
|
|
388
572
|
}
|
|
573
|
+
debug(`[check] 数据一致性检查完成: invalidProjects=${result.invalidProjects.length}, danglingLinks=${result.danglingLinks.length}, orphanLibraries=${result.orphanLibraries.length}, uncachedDependencies=${result.missingLibraries.length}, staleReferences=${result.staleReferences.length}, corruptedStores=${result.corruptedStores.length}`);
|
|
389
574
|
return result;
|
|
390
575
|
}
|
|
391
576
|
// ============ 报告渲染 ============
|
|
@@ -415,10 +600,9 @@ function renderReport(result) {
|
|
|
415
600
|
renderCheck('孤立库', integ.orphanLibraries.length === 0, integ.orphanLibraries.length === 0
|
|
416
601
|
? '无'
|
|
417
602
|
: `${integ.orphanLibraries.length} 个未登记 (${formatSize(result.summary.reclaimableSize)})`, false);
|
|
418
|
-
renderCheck('
|
|
603
|
+
renderCheck('本地未缓存依赖', integ.missingLibraries.length === 0, integ.missingLibraries.length === 0
|
|
419
604
|
? '无'
|
|
420
|
-
: `${integ.missingLibraries.length}
|
|
421
|
-
);
|
|
605
|
+
: `${integ.missingLibraries.length} 项(项目需要时可运行 td link)`, integ.missingLibraries.length > 0);
|
|
422
606
|
renderCheck('引用关系', integ.staleReferences.length === 0, integ.staleReferences.length === 0
|
|
423
607
|
? '一致'
|
|
424
608
|
: `${integ.staleReferences.length} 个失效引用`, false);
|
|
@@ -426,10 +610,32 @@ function renderReport(result) {
|
|
|
426
610
|
? '完整'
|
|
427
611
|
: `${integ.corruptedStores.length} 个损坏的 Store 条目`, false);
|
|
428
612
|
blank();
|
|
613
|
+
info('空间一致性');
|
|
614
|
+
separator();
|
|
615
|
+
if (result.space.error) {
|
|
616
|
+
renderCheck('空间审计', false, result.space.error, false);
|
|
617
|
+
}
|
|
618
|
+
else if (result.space.totals) {
|
|
619
|
+
const totals = result.space.totals;
|
|
620
|
+
renderCheck('空间审计', result.space.issues.length === 0, result.space.issues.length === 0
|
|
621
|
+
? `实际占用 ${formatSize(totals.physical.bytes)},登记容量一致`
|
|
622
|
+
: `${result.space.issues.length} 个差异,实际 ${formatSize(totals.physical.bytes)},登记 ${formatSize(totals.registered.bytes)}`, false);
|
|
623
|
+
for (const issue of result.space.issues.slice(0, 20)) {
|
|
624
|
+
debug(`[space] audit 差异: kind=${issue.kind}, lib=${issue.id.libName}, commit=${issue.id.commit}, platform=${issue.platform ?? 'all'}, registered=${issue.registeredBytes ?? 'unknown'}, measured=${issue.measuredBytes ?? 'unknown'}, message=${issue.message}`);
|
|
625
|
+
}
|
|
626
|
+
if (result.space.issues.length > 20) {
|
|
627
|
+
debug(`[space] audit 差异省略: remaining=${result.space.issues.length - 20}`);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
blank();
|
|
429
631
|
separator();
|
|
430
632
|
// 汇总
|
|
431
|
-
const { envErrors, envWarnings, integrityIssues, reclaimableSize } = result.summary;
|
|
432
|
-
if (envErrors === 0 &&
|
|
633
|
+
const { envErrors, envWarnings, integrityIssues, uncachedDependencies, reclaimableSize, spaceIssues, } = result.summary;
|
|
634
|
+
if (envErrors === 0 &&
|
|
635
|
+
envWarnings === 0 &&
|
|
636
|
+
integrityIssues === 0 &&
|
|
637
|
+
uncachedDependencies === 0 &&
|
|
638
|
+
spaceIssues === 0) {
|
|
433
639
|
success('系统健康,无问题');
|
|
434
640
|
}
|
|
435
641
|
else {
|
|
@@ -440,6 +646,10 @@ function renderReport(result) {
|
|
|
440
646
|
parts.push(`${envWarnings} 个警告`);
|
|
441
647
|
if (integrityIssues > 0)
|
|
442
648
|
parts.push(`${integrityIssues} 个数据问题`);
|
|
649
|
+
if (uncachedDependencies > 0)
|
|
650
|
+
parts.push(`${uncachedDependencies} 项本地未缓存依赖`);
|
|
651
|
+
if (spaceIssues > 0)
|
|
652
|
+
parts.push(`${spaceIssues} 个空间差异`);
|
|
443
653
|
if (reclaimableSize > 0)
|
|
444
654
|
parts.push(`可回收 ${formatSize(reclaimableSize)}`);
|
|
445
655
|
warn(`发现问题: ${parts.join(', ')}`);
|
|
@@ -499,43 +709,55 @@ function renderCheck(name, ok, message, isWarning) {
|
|
|
499
709
|
}
|
|
500
710
|
}
|
|
501
711
|
// ============ 交互式修复 ============
|
|
502
|
-
|
|
712
|
+
function hasSpaceCalibrationChoice(space) {
|
|
713
|
+
const plan = space.calibrationPlan;
|
|
714
|
+
return Boolean(plan && (plan.actions.length > 0 || plan.unregistered.deletableCount > 0));
|
|
715
|
+
}
|
|
716
|
+
function renderUncachedDependencyHint(issues) {
|
|
717
|
+
if (issues.missingLibraries.length === 0)
|
|
718
|
+
return;
|
|
503
719
|
blank();
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
720
|
+
hint(`${issues.missingLibraries.length} 项本地未缓存依赖已保留;项目需要这些依赖时可运行 td link`);
|
|
721
|
+
}
|
|
722
|
+
async function interactiveCheck(result, options) {
|
|
723
|
+
while (true) {
|
|
724
|
+
blank();
|
|
725
|
+
const action = await selectWithCancel({
|
|
726
|
+
message: '选择操作',
|
|
727
|
+
choices: [
|
|
728
|
+
{ name: '修复所有可修复问题', value: 'fix-all' },
|
|
729
|
+
{ name: '选择性修复', value: 'select' },
|
|
730
|
+
{ name: '查看详情', value: 'detail' },
|
|
731
|
+
{ name: colorize('← 退出', 'dim'), value: 'exit' },
|
|
732
|
+
],
|
|
733
|
+
});
|
|
734
|
+
if (action === PROMPT_CANCELLED) {
|
|
735
|
+
info('已取消');
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
debug(`[check] 交互操作: action=${action}`);
|
|
739
|
+
if (action === 'detail') {
|
|
740
|
+
showDetails(result);
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
if (action === 'exit')
|
|
744
|
+
return;
|
|
745
|
+
if (action === 'select') {
|
|
746
|
+
await selectiveFix(result, options);
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
await fixAllIssues(result.integrity, result.space, {
|
|
750
|
+
force: true,
|
|
751
|
+
calibrateSpace: hasSpaceCalibrationChoice(result.space),
|
|
752
|
+
deleteUnregistered: false,
|
|
753
|
+
verifyStoreIntegrity: options.integrity,
|
|
754
|
+
});
|
|
755
|
+
renderUncachedDependencyHint(result.integrity);
|
|
516
756
|
return;
|
|
517
757
|
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
// 缺失库无法自动修复,需要提示用户
|
|
522
|
-
if (issues.missingLibraries.length > 0) {
|
|
523
|
-
blank();
|
|
524
|
-
hint(`缺失库 (${issues.missingLibraries.length} 个) 需要通过 td link 重新下载`);
|
|
525
|
-
}
|
|
526
|
-
break;
|
|
527
|
-
case 'select':
|
|
528
|
-
await selectiveFix(issues, options);
|
|
529
|
-
break;
|
|
530
|
-
case 'detail':
|
|
531
|
-
showDetails(issues);
|
|
532
|
-
await interactiveCheck(issues, options);
|
|
533
|
-
break;
|
|
534
|
-
case 'exit':
|
|
535
|
-
break;
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
async function selectiveFix(issues, options) {
|
|
758
|
+
}
|
|
759
|
+
async function selectiveFix(result, options) {
|
|
760
|
+
const issues = result.integrity;
|
|
539
761
|
// 构建选项
|
|
540
762
|
const choices = [];
|
|
541
763
|
if (issues.invalidProjects.length > 0) {
|
|
@@ -574,6 +796,13 @@ async function selectiveFix(issues, options) {
|
|
|
574
796
|
checked: true,
|
|
575
797
|
});
|
|
576
798
|
}
|
|
799
|
+
if (hasSpaceCalibrationChoice(result.space)) {
|
|
800
|
+
choices.push({
|
|
801
|
+
name: `校准 ${result.space.issues.length} 个空间差异`,
|
|
802
|
+
value: 'spaceCalibration',
|
|
803
|
+
checked: true,
|
|
804
|
+
});
|
|
805
|
+
}
|
|
577
806
|
if (choices.length === 0) {
|
|
578
807
|
success('没有可修复的问题');
|
|
579
808
|
return;
|
|
@@ -601,7 +830,8 @@ async function selectiveFix(issues, options) {
|
|
|
601
830
|
orphanLibraries: selected.includes('orphanLibraries')
|
|
602
831
|
? issues.orphanLibraries
|
|
603
832
|
: [],
|
|
604
|
-
|
|
833
|
+
// 本地未缓存依赖是用户允许的缓存状态,修复流程不下载也不移除依赖记录。
|
|
834
|
+
missingLibraries: [],
|
|
605
835
|
staleReferences: selected.includes('staleReferences')
|
|
606
836
|
? issues.staleReferences
|
|
607
837
|
: [],
|
|
@@ -609,14 +839,76 @@ async function selectiveFix(issues, options) {
|
|
|
609
839
|
? issues.corruptedStores
|
|
610
840
|
: [],
|
|
611
841
|
};
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
842
|
+
const calibrateSpace = selected.includes('spaceCalibration');
|
|
843
|
+
let deleteUnregistered = false;
|
|
844
|
+
const spacePlan = result.space.calibrationPlan;
|
|
845
|
+
if (calibrateSpace &&
|
|
846
|
+
spacePlan &&
|
|
847
|
+
spacePlan.unregistered.totalCount > 0 &&
|
|
848
|
+
spacePlan.unregistered.deletableCount > 0) {
|
|
849
|
+
blank();
|
|
850
|
+
const unregisteredAction = await selectWithCancel({
|
|
851
|
+
message: '未登记平台如何处理?',
|
|
852
|
+
choices: [
|
|
853
|
+
{
|
|
854
|
+
name: '暂不处理',
|
|
855
|
+
value: 'keep',
|
|
856
|
+
description: `保留全部 ${spacePlan.unregistered.totalCount} 项内容`,
|
|
857
|
+
},
|
|
858
|
+
{
|
|
859
|
+
name: '删除无项目引用的内容',
|
|
860
|
+
value: 'delete-unreferenced',
|
|
861
|
+
description: `删除 ${spacePlan.unregistered.deletableCount} 项,保留 ${spacePlan.unregistered.protectedCount} 项`,
|
|
862
|
+
},
|
|
863
|
+
],
|
|
864
|
+
});
|
|
865
|
+
if (unregisteredAction === PROMPT_CANCELLED) {
|
|
866
|
+
info('已取消');
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
deleteUnregistered = unregisteredAction === 'delete-unreferenced';
|
|
870
|
+
}
|
|
871
|
+
debug(`[check] 选择性修复输入: selected=${selected.join(',')}, calibrateSpace=${calibrateSpace}, unregisteredAction=${deleteUnregistered ? 'delete-unreferenced' : 'keep'}`);
|
|
872
|
+
await fixAllIssues(toFix, result.space, {
|
|
873
|
+
force: options.force,
|
|
874
|
+
calibrateSpace,
|
|
875
|
+
deleteUnregistered,
|
|
876
|
+
verifyStoreIntegrity: options.integrity,
|
|
877
|
+
});
|
|
878
|
+
renderUncachedDependencyHint(issues);
|
|
879
|
+
}
|
|
880
|
+
function formatSignedSize(bytes) {
|
|
881
|
+
const sign = bytes > 0 ? '+' : bytes < 0 ? '-' : '';
|
|
882
|
+
return `${sign}${formatSize(Math.abs(bytes))}`;
|
|
883
|
+
}
|
|
884
|
+
function showSpaceDetails(space) {
|
|
885
|
+
if (space.error) {
|
|
886
|
+
info('空间审计:');
|
|
887
|
+
warn(` ${space.error}`);
|
|
615
888
|
blank();
|
|
616
|
-
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
const plan = space.calibrationPlan;
|
|
892
|
+
if (!plan || space.issues.length === 0)
|
|
893
|
+
return;
|
|
894
|
+
info(`空间差异 (${space.issues.length}):`);
|
|
895
|
+
info(` 登记容量变化 ${plan.summary.changedCount} 项,合计 ${formatSignedSize(plan.summary.changedDifferenceBytes)}`);
|
|
896
|
+
info(` 缺失登记 ${plan.summary.missingCount} 项,登记容量 ${formatSize(plan.summary.missingRegisteredBytes)}`);
|
|
897
|
+
info(` 重复共享登记 ${plan.summary.duplicateSharedCount} 项`);
|
|
898
|
+
info(` 未登记平台 ${plan.unregistered.totalCount} 项,合计 ${formatSize(plan.unregistered.totalBytes)}`);
|
|
899
|
+
if (plan.unregistered.protectedCount > 0) {
|
|
900
|
+
info(` 引用保护 ${plan.unregistered.protectedCount} 项`);
|
|
901
|
+
}
|
|
902
|
+
const otherIssues = space.issues.filter((issue) => !['changed', 'missing', 'duplicate-record', 'unregistered'].includes(issue.kind));
|
|
903
|
+
if (otherIssues.length > 0)
|
|
904
|
+
info(` 其他空间异常 ${otherIssues.length} 项`);
|
|
905
|
+
if (plan.summary.ignoredArtifactCount > 0) {
|
|
906
|
+
info(` 系统文件 ${plan.summary.ignoredArtifactCount} 项,将忽略并保留`);
|
|
617
907
|
}
|
|
908
|
+
blank();
|
|
618
909
|
}
|
|
619
|
-
function showDetails(
|
|
910
|
+
function showDetails(result) {
|
|
911
|
+
const issues = result.integrity;
|
|
620
912
|
blank();
|
|
621
913
|
title('问题详情');
|
|
622
914
|
blank();
|
|
@@ -642,7 +934,7 @@ function showDetails(issues) {
|
|
|
642
934
|
blank();
|
|
643
935
|
}
|
|
644
936
|
if (issues.missingLibraries.length > 0) {
|
|
645
|
-
info(
|
|
937
|
+
info(`本地未缓存依赖 (${issues.missingLibraries.length}):`);
|
|
646
938
|
for (const lib of issues.missingLibraries) {
|
|
647
939
|
hint(` - ${lib.libName}/${lib.commit.slice(0, 7)} (引用自 ${lib.project})`);
|
|
648
940
|
}
|
|
@@ -669,22 +961,61 @@ function showDetails(issues) {
|
|
|
669
961
|
}
|
|
670
962
|
blank();
|
|
671
963
|
}
|
|
964
|
+
showSpaceDetails(result.space);
|
|
672
965
|
}
|
|
673
|
-
|
|
674
|
-
|
|
966
|
+
function integrityRepairCount(issues) {
|
|
967
|
+
return (issues.invalidProjects.length +
|
|
675
968
|
issues.danglingLinks.length +
|
|
676
969
|
issues.orphanLibraries.length +
|
|
677
970
|
issues.staleReferences.length +
|
|
678
|
-
issues.corruptedStores.length;
|
|
679
|
-
|
|
971
|
+
issues.corruptedStores.length);
|
|
972
|
+
}
|
|
973
|
+
function retainCurrentIssues(selected, current, keyOf) {
|
|
974
|
+
const selectedKeys = new Set(selected.map(keyOf));
|
|
975
|
+
return current.filter((issue) => selectedKeys.has(keyOf(issue)));
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* 只保留用户确认过且在取得全局锁后仍然存在的问题。
|
|
979
|
+
* 新出现的问题不会被本次修复附带处理。
|
|
980
|
+
*/
|
|
981
|
+
async function revalidateIntegritySelection(selected, verifyStoreIntegrity) {
|
|
982
|
+
const current = await checkIntegrity({ integrity: verifyStoreIntegrity });
|
|
983
|
+
const revalidated = {
|
|
984
|
+
invalidProjects: retainCurrentIssues(selected.invalidProjects, current.invalidProjects, (issue) => JSON.stringify([issue.hash, issue.path])),
|
|
985
|
+
danglingLinks: retainCurrentIssues(selected.danglingLinks, current.danglingLinks, (issue) => JSON.stringify([
|
|
986
|
+
issue.path,
|
|
987
|
+
issue.projectHash,
|
|
988
|
+
issue.dep.libName,
|
|
989
|
+
issue.dep.commit,
|
|
990
|
+
issue.dep.platform,
|
|
991
|
+
issue.dep.linkedPath,
|
|
992
|
+
issue.dep.scope ?? '',
|
|
993
|
+
])),
|
|
994
|
+
orphanLibraries: retainCurrentIssues(selected.orphanLibraries, current.orphanLibraries, (issue) => JSON.stringify([issue.libName, issue.commit, issue.path])),
|
|
995
|
+
missingLibraries: [],
|
|
996
|
+
staleReferences: retainCurrentIssues(selected.staleReferences, current.staleReferences, (issue) => JSON.stringify([issue.libKey, issue.projectHash])),
|
|
997
|
+
corruptedStores: retainCurrentIssues(selected.corruptedStores, current.corruptedStores, (issue) => JSON.stringify([issue.libName, issue.commit, issue.platform, issue.kind ?? 'integrity'])),
|
|
998
|
+
};
|
|
999
|
+
debug(`[check] 锁内重新检查完成: selected=${integrityRepairCount(selected)}, retained=${integrityRepairCount(revalidated)}, currentUncachedDependencies=${current.missingLibraries.length}, invalidProjects=${revalidated.invalidProjects.length}, danglingLinks=${revalidated.danglingLinks.length}, orphanLibraries=${revalidated.orphanLibraries.length}, staleReferences=${revalidated.staleReferences.length}, corruptedStores=${revalidated.corruptedStores.length}`);
|
|
1000
|
+
return revalidated;
|
|
1001
|
+
}
|
|
1002
|
+
async function fixAllIssues(issues, space, options) {
|
|
1003
|
+
const totalIssues = integrityRepairCount(issues);
|
|
1004
|
+
const spaceIssueCount = options.calibrateSpace ? space.issues.length : 0;
|
|
1005
|
+
if (totalIssues === 0 && spaceIssueCount === 0) {
|
|
680
1006
|
success('没有需要修复的问题');
|
|
681
1007
|
return;
|
|
682
1008
|
}
|
|
1009
|
+
debug(`[check] 修复请求: integrityIssues=${totalIssues}, calibrateSpace=${options.calibrateSpace}, reportedSpaceIssues=${spaceIssueCount}, unregisteredAction=${options.deleteUnregistered ? 'delete-unreferenced' : 'keep'}, force=${options.force}, sourceSnapshot=${space.snapshotId ?? 'none'}`);
|
|
683
1010
|
// 确认
|
|
684
1011
|
if (!options.force) {
|
|
685
1012
|
blank();
|
|
1013
|
+
const targets = [
|
|
1014
|
+
...(totalIssues > 0 ? [`${totalIssues} 个数据问题`] : []),
|
|
1015
|
+
...(spaceIssueCount > 0 ? [`${spaceIssueCount} 个空间差异`] : []),
|
|
1016
|
+
];
|
|
686
1017
|
const confirmed = await confirmWithCancel({
|
|
687
|
-
message:
|
|
1018
|
+
message: `确认处理${targets.join('和')}?`,
|
|
688
1019
|
default: false,
|
|
689
1020
|
});
|
|
690
1021
|
if (confirmed === PROMPT_CANCELLED || !confirmed) {
|
|
@@ -696,19 +1027,167 @@ async function fixAllIssues(issues, options) {
|
|
|
696
1027
|
separator();
|
|
697
1028
|
info('正在修复...');
|
|
698
1029
|
blank();
|
|
1030
|
+
await withGlobalLock(async () => {
|
|
1031
|
+
if (totalIssues > 0) {
|
|
1032
|
+
info('正在重新确认待修复的数据问题...');
|
|
1033
|
+
const revalidated = await revalidateIntegritySelection(issues, options.verifyStoreIntegrity);
|
|
1034
|
+
if (integrityRepairCount(revalidated) === 0) {
|
|
1035
|
+
success('待修复的数据问题状态已变化,本次无需处理');
|
|
1036
|
+
}
|
|
1037
|
+
else {
|
|
1038
|
+
const repairResult = await applyAllIssueFixes(revalidated);
|
|
1039
|
+
if (repairResult.failed > 0)
|
|
1040
|
+
process.exitCode = 1;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
if (options.calibrateSpace) {
|
|
1044
|
+
try {
|
|
1045
|
+
await applySpaceCalibration(space, options.deleteUnregistered);
|
|
1046
|
+
}
|
|
1047
|
+
catch (err) {
|
|
1048
|
+
const message = err.message;
|
|
1049
|
+
error(`空间校准失败: ${message}`);
|
|
1050
|
+
debug(`[check] 空间校准异常: sourceSnapshot=${space.snapshotId ?? 'none'}, deleteUnregistered=${options.deleteUnregistered}, error=${message}`);
|
|
1051
|
+
process.exitCode = 1;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
async function applySpaceCalibration(sourceSpace, deleteUnregistered) {
|
|
1057
|
+
const registry = getRegistry();
|
|
1058
|
+
await registry.load();
|
|
1059
|
+
const storePath = await store.getStorePath();
|
|
1060
|
+
const service = new SpaceService(registry, storePath);
|
|
1061
|
+
const cleanupExecutor = service.createCleanupExecutor();
|
|
1062
|
+
const recovered = await cleanupExecutor.recoverPending();
|
|
1063
|
+
debug(`[check] 空间校准开始: store=${storePath}, sourceSnapshot=${sourceSpace.snapshotId ?? 'none'}, sourceIssues=${sourceSpace.issues.length}, unregisteredAction=${deleteUnregistered ? 'delete-unreferenced' : 'keep'}, recovered=${recovered}`);
|
|
1064
|
+
if (recovered > 0)
|
|
1065
|
+
info(`恢复了 ${recovered} 个中断清理记录`);
|
|
1066
|
+
const planningProgress = createSpaceProgressRenderer('正在重新核对空间差异');
|
|
1067
|
+
let snapshot;
|
|
1068
|
+
try {
|
|
1069
|
+
snapshot = await service.buildSnapshot('audit', undefined, planningProgress.update);
|
|
1070
|
+
}
|
|
1071
|
+
finally {
|
|
1072
|
+
planningProgress.close();
|
|
1073
|
+
}
|
|
1074
|
+
const plan = service.buildCalibrationPlan(snapshot);
|
|
1075
|
+
const willDelete = deleteUnregistered && plan.unregistered.deletableCount > 0;
|
|
1076
|
+
debug(`[check] 空间校准计划: plan=${plan.id}, snapshot=${snapshot.id}, metadataActions=${plan.actions.length}, changed=${plan.summary.changedCount}, missing=${plan.summary.missingCount}, duplicates=${plan.summary.duplicateSharedCount}, unregistered=${plan.unregistered.totalCount}, deletable=${plan.unregistered.deletableCount}, protected=${plan.unregistered.protectedCount}, willDelete=${willDelete}`);
|
|
1077
|
+
if (plan.actions.length === 0 && !willDelete) {
|
|
1078
|
+
if (plan.unregistered.totalCount > 0) {
|
|
1079
|
+
success(`登记数据无需调整,保留 ${plan.unregistered.totalCount} 项未登记平台`);
|
|
1080
|
+
}
|
|
1081
|
+
else {
|
|
1082
|
+
success('空间登记与 Store 实际状态一致');
|
|
1083
|
+
}
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
const stageProgress = {
|
|
1087
|
+
'validate-plan': createSpaceProgressRenderer('正在验证校准计划'),
|
|
1088
|
+
'verify-result': createSpaceProgressRenderer('正在验证校准结果'),
|
|
1089
|
+
};
|
|
1090
|
+
let calibrationResult;
|
|
1091
|
+
try {
|
|
1092
|
+
calibrationResult = await service.createCalibrationExecutor().execute(plan, {
|
|
1093
|
+
deleteUnregistered,
|
|
1094
|
+
onProgress: (stage, progress) => stageProgress[stage].update(progress),
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
finally {
|
|
1098
|
+
stageProgress['validate-plan'].close();
|
|
1099
|
+
stageProgress['verify-result'].close();
|
|
1100
|
+
}
|
|
1101
|
+
const remainingPlan = service.buildCalibrationPlan(calibrationResult.finalSnapshot);
|
|
1102
|
+
const cleanupFailed = calibrationResult.cleanup != null && calibrationResult.cleanup.status !== 'completed';
|
|
1103
|
+
if (cleanupFailed) {
|
|
1104
|
+
process.exitCode = 1;
|
|
1105
|
+
warn(`空间校准部分完成: 更新 ${calibrationResult.metadataActionCount} 项登记数据,未登记平台删除成功 ${calibrationResult.cleanup.succeeded.length} 项、失败 ${calibrationResult.cleanup.failed.length} 项`);
|
|
1106
|
+
for (const failure of calibrationResult.cleanup.failed) {
|
|
1107
|
+
error(`未登记平台删除失败: ${failure.id.libName}/${failure.id.commit.slice(0, 7)}/${failure.platform ?? 'all'} - ${failure.message}`);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
else {
|
|
1111
|
+
success(`空间校准完成: 更新 ${calibrationResult.metadataActionCount} 项登记数据`);
|
|
1112
|
+
}
|
|
1113
|
+
if (calibrationResult.cleanup && !cleanupFailed) {
|
|
1114
|
+
info(`未登记平台清理: 删除 ${calibrationResult.cleanup.succeeded.length} 项,释放 ${formatSize(calibrationResult.cleanup.released.bytes)}`);
|
|
1115
|
+
}
|
|
1116
|
+
if (remainingPlan.unregistered.totalCount > 0) {
|
|
1117
|
+
info(`保留未登记平台: ${remainingPlan.unregistered.totalCount} 项`);
|
|
1118
|
+
}
|
|
1119
|
+
if (remainingPlan.actions.length > 0) {
|
|
1120
|
+
process.exitCode = 1;
|
|
1121
|
+
warn(`重新检查仍发现 ${remainingPlan.actions.length} 项登记差异,请查看详细日志`);
|
|
1122
|
+
}
|
|
1123
|
+
debug(`[check] 空间校准完成: plan=${plan.id}, finalSnapshot=${calibrationResult.finalSnapshot.id}, metadataActions=${calibrationResult.metadataActionCount}, cleanupStatus=${calibrationResult.cleanup?.status ?? 'skipped'}, cleanupSucceeded=${calibrationResult.cleanup?.succeeded.length ?? 0}, cleanupFailed=${calibrationResult.cleanup?.failed.length ?? 0}, remainingMetadata=${remainingPlan.actions.length}, remainingUnregistered=${remainingPlan.unregistered.totalCount}, finalDifference=${calibrationResult.finalSnapshot.totals.differenceBytes}`);
|
|
1124
|
+
}
|
|
1125
|
+
function commitKey(libName, commit) {
|
|
1126
|
+
return JSON.stringify([libName, commit]);
|
|
1127
|
+
}
|
|
1128
|
+
function buildCorruptedSelections(corrupted, includeShared) {
|
|
1129
|
+
const grouped = new Map();
|
|
1130
|
+
for (const item of corrupted) {
|
|
1131
|
+
const key = commitKey(item.libName, item.commit);
|
|
1132
|
+
const group = grouped.get(key) ?? {
|
|
1133
|
+
id: { libName: item.libName, commit: item.commit },
|
|
1134
|
+
platforms: new Set(),
|
|
1135
|
+
};
|
|
1136
|
+
if (!includeShared)
|
|
1137
|
+
group.platforms.add(item.platform);
|
|
1138
|
+
grouped.set(key, group);
|
|
1139
|
+
}
|
|
1140
|
+
return [...grouped.values()].map((group) => ({
|
|
1141
|
+
id: group.id,
|
|
1142
|
+
platforms: includeShared ? 'all' : [...group.platforms],
|
|
1143
|
+
...(includeShared ? { includeShared: true } : {}),
|
|
1144
|
+
}));
|
|
1145
|
+
}
|
|
1146
|
+
async function executeRepairPlan(spaceService, executor, selections, options) {
|
|
1147
|
+
if (selections.length === 0)
|
|
1148
|
+
return null;
|
|
1149
|
+
const plan = await spaceService.buildCleanupPlan({
|
|
1150
|
+
reason: 'repair',
|
|
1151
|
+
selections,
|
|
1152
|
+
requireUnreferenced: options.requireUnreferenced,
|
|
1153
|
+
allowUnknownContent: options.allowUnknownContent,
|
|
1154
|
+
});
|
|
1155
|
+
for (const warningMessage of plan.warnings)
|
|
1156
|
+
warn(`[space] ${warningMessage}`);
|
|
1157
|
+
debug(`[check] 调用统一清理执行器: category=${options.category}, plan=${plan.id}, selections=${selections.length}, groups=${plan.groups.length}, expected=${plan.expectedRelease.bytes}`);
|
|
1158
|
+
if (plan.groups.length === 0)
|
|
1159
|
+
return null;
|
|
1160
|
+
const result = await executor.execute(plan);
|
|
1161
|
+
debug(`[check] 清理执行返回: category=${options.category}, plan=${plan.id}, status=${result.status}, succeeded=${result.succeeded.length}, failed=${result.failed.length}, released=${result.released.bytes}`);
|
|
1162
|
+
return result;
|
|
1163
|
+
}
|
|
1164
|
+
function resolvedIssueCount(result, issues) {
|
|
1165
|
+
if (!result)
|
|
1166
|
+
return 0;
|
|
1167
|
+
return issues.filter((issue) => result.succeeded.some((group) => group.id.libName === issue.libName
|
|
1168
|
+
&& group.id.commit === issue.commit
|
|
1169
|
+
&& (group.platform == null || issue.platform == null || group.platform === issue.platform))).length;
|
|
1170
|
+
}
|
|
1171
|
+
async function applyAllIssueFixes(issues) {
|
|
699
1172
|
const registry = getRegistry();
|
|
700
1173
|
await registry.load();
|
|
701
1174
|
const storePath = await store.getStorePath();
|
|
1175
|
+
const spaceService = new SpaceService(registry, storePath);
|
|
1176
|
+
const executor = spaceService.createCleanupExecutor();
|
|
1177
|
+
const recovered = await executor.recoverPending();
|
|
1178
|
+
debug(`[check] 修复开始: invalidProjects=${issues.invalidProjects.length}, danglingLinks=${issues.danglingLinks.length}, orphanLibraries=${issues.orphanLibraries.length}, staleReferences=${issues.staleReferences.length}, corruptedStores=${issues.corruptedStores.length}, recovered=${recovered}`);
|
|
702
1179
|
let fixed = 0;
|
|
1180
|
+
let failed = 0;
|
|
703
1181
|
// 1. 清理无效项目
|
|
704
1182
|
for (const p of issues.invalidProjects) {
|
|
705
1183
|
try {
|
|
706
1184
|
registry.removeProject(p.hash);
|
|
707
|
-
success(
|
|
1185
|
+
success(`清理项目: ${p.path}`);
|
|
708
1186
|
fixed++;
|
|
709
1187
|
}
|
|
710
1188
|
catch (err) {
|
|
711
1189
|
error(`[err] 清理项目失败: ${p.path} - ${err.message}`);
|
|
1190
|
+
failed++;
|
|
712
1191
|
}
|
|
713
1192
|
}
|
|
714
1193
|
// 2. 移除悬挂链接
|
|
@@ -717,7 +1196,11 @@ async function fixAllIssues(issues, options) {
|
|
|
717
1196
|
await fs.unlink(link.path);
|
|
718
1197
|
const project = registry.getProject(link.projectHash);
|
|
719
1198
|
if (project) {
|
|
720
|
-
project.dependencies = project.dependencies.filter((
|
|
1199
|
+
project.dependencies = project.dependencies.filter((dependency) => !(dependency.libName === link.dep.libName &&
|
|
1200
|
+
dependency.commit === link.dep.commit &&
|
|
1201
|
+
dependency.platform === link.dep.platform &&
|
|
1202
|
+
dependency.linkedPath === link.dep.linkedPath &&
|
|
1203
|
+
(dependency.scope ?? '') === (link.dep.scope ?? '')));
|
|
721
1204
|
registry.updateProject(link.projectHash, {
|
|
722
1205
|
dependencies: project.dependencies,
|
|
723
1206
|
});
|
|
@@ -727,24 +1210,32 @@ async function fixAllIssues(issues, options) {
|
|
|
727
1210
|
}
|
|
728
1211
|
catch (err) {
|
|
729
1212
|
error(`[err] 移除链接失败: ${link.path} - ${err.message}`);
|
|
1213
|
+
failed++;
|
|
730
1214
|
}
|
|
731
1215
|
}
|
|
732
|
-
// 3.
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
1216
|
+
// 3. 删除孤立库。显式修复允许清理该版本中的未登记内容。
|
|
1217
|
+
const orphanSelections = [...new Map(issues.orphanLibraries.map((lib) => [
|
|
1218
|
+
commitKey(lib.libName, lib.commit),
|
|
1219
|
+
{ id: { libName: lib.libName, commit: lib.commit }, platforms: 'all' },
|
|
1220
|
+
])).values()];
|
|
1221
|
+
try {
|
|
1222
|
+
const result = await executeRepairPlan(spaceService, executor, orphanSelections, {
|
|
1223
|
+
requireUnreferenced: true,
|
|
1224
|
+
allowUnknownContent: true,
|
|
1225
|
+
category: 'orphan-library',
|
|
1226
|
+
});
|
|
1227
|
+
fixed += resolvedIssueCount(result, issues.orphanLibraries);
|
|
1228
|
+
for (const group of result?.succeeded ?? []) {
|
|
1229
|
+
success(`[ok] 删除孤立库: ${group.id.libName}/${group.id.commit.slice(0, 7)}`);
|
|
744
1230
|
}
|
|
745
|
-
|
|
746
|
-
error(`[err] 删除孤立库失败: ${
|
|
1231
|
+
for (const failure of result?.failed ?? []) {
|
|
1232
|
+
error(`[err] 删除孤立库失败: ${failure.id.libName}/${failure.id.commit.slice(0, 7)} - ${failure.message}`);
|
|
747
1233
|
}
|
|
1234
|
+
failed += result?.failed.length ?? (orphanSelections.length > 0 ? 1 : 0);
|
|
1235
|
+
}
|
|
1236
|
+
catch (err) {
|
|
1237
|
+
error(`[err] 删除孤立库失败: ${err.message}`);
|
|
1238
|
+
failed++;
|
|
748
1239
|
}
|
|
749
1240
|
// 4. 移除失效引用
|
|
750
1241
|
for (const ref of issues.staleReferences) {
|
|
@@ -754,6 +1245,7 @@ async function fixAllIssues(issues, options) {
|
|
|
754
1245
|
const colonIndex = ref.libKey.lastIndexOf(':');
|
|
755
1246
|
if (colonIndex === -1) {
|
|
756
1247
|
error(`[err] 无效的 libKey: ${ref.libKey}`);
|
|
1248
|
+
failed++;
|
|
757
1249
|
continue;
|
|
758
1250
|
}
|
|
759
1251
|
const libName = ref.libKey.slice(0, colonIndex);
|
|
@@ -767,37 +1259,62 @@ async function fixAllIssues(issues, options) {
|
|
|
767
1259
|
}
|
|
768
1260
|
catch (err) {
|
|
769
1261
|
error(`[err] 移除引用失败: ${ref.libKey} - ${err.message}`);
|
|
1262
|
+
failed++;
|
|
770
1263
|
}
|
|
771
1264
|
}
|
|
772
|
-
// 5. 清理损坏的 Store
|
|
773
|
-
|
|
1265
|
+
// 5. 清理损坏的 Store 条目。共享冲突整版本处理,其他问题按平台处理。
|
|
1266
|
+
const orphanKeys = new Set(issues.orphanLibraries.map((item) => commitKey(item.libName, item.commit)));
|
|
1267
|
+
const sharedCorruptions = issues.corruptedStores.filter((item) => (item.kind === 'shared_platform_conflict' || item.platform === SHARED_PLATFORM)
|
|
1268
|
+
&& !orphanKeys.has(commitKey(item.libName, item.commit)));
|
|
1269
|
+
const sharedCorruptionKeys = new Set(sharedCorruptions.map((item) => commitKey(item.libName, item.commit)));
|
|
1270
|
+
const platformCorruptions = issues.corruptedStores.filter((item) => !orphanKeys.has(commitKey(item.libName, item.commit))
|
|
1271
|
+
&& !sharedCorruptionKeys.has(commitKey(item.libName, item.commit)));
|
|
1272
|
+
for (const cleanup of [
|
|
1273
|
+
{
|
|
1274
|
+
items: issues.corruptedStores.filter((item) => sharedCorruptionKeys.has(commitKey(item.libName, item.commit))),
|
|
1275
|
+
selections: buildCorruptedSelections(sharedCorruptions, true),
|
|
1276
|
+
allowUnknownContent: false,
|
|
1277
|
+
category: 'shared-corruption',
|
|
1278
|
+
},
|
|
1279
|
+
{
|
|
1280
|
+
items: platformCorruptions,
|
|
1281
|
+
selections: buildCorruptedSelections(platformCorruptions, false),
|
|
1282
|
+
allowUnknownContent: false,
|
|
1283
|
+
category: 'corrupted-platform',
|
|
1284
|
+
},
|
|
1285
|
+
]) {
|
|
774
1286
|
try {
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
1287
|
+
const result = await executeRepairPlan(spaceService, executor, cleanup.selections, {
|
|
1288
|
+
requireUnreferenced: false,
|
|
1289
|
+
allowUnknownContent: cleanup.allowUnknownContent,
|
|
1290
|
+
category: cleanup.category,
|
|
1291
|
+
});
|
|
1292
|
+
fixed += resolvedIssueCount(result, cleanup.items);
|
|
1293
|
+
for (const group of result?.succeeded ?? []) {
|
|
1294
|
+
const suffix = group.platform ? `/${group.platform}` : ' (移除整个 commit 缓存)';
|
|
1295
|
+
success(`清理损坏 Store: ${group.id.libName}/${group.id.commit.slice(0, 7)}${suffix}`);
|
|
783
1296
|
}
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
registry.removeStore(storeKey);
|
|
787
|
-
const platformPath = path.join(storePath, item.libName, item.commit, item.platform);
|
|
788
|
-
await fs.rm(platformPath, { recursive: true, force: true });
|
|
789
|
-
success(`[ok] 清理损坏 Store: ${item.libName}/${item.commit.slice(0, 7)}/${item.platform}`);
|
|
1297
|
+
for (const failure of result?.failed ?? []) {
|
|
1298
|
+
error(`清理损坏 Store 失败: ${failure.id.libName}/${failure.id.commit.slice(0, 7)}/${failure.platform ?? 'all'} - ${failure.message}`);
|
|
790
1299
|
}
|
|
791
|
-
|
|
1300
|
+
failed += result?.failed.length ?? (cleanup.selections.length > 0 ? 1 : 0);
|
|
792
1301
|
}
|
|
793
1302
|
catch (err) {
|
|
794
|
-
error(
|
|
1303
|
+
error(`清理损坏 Store 失败: category=${cleanup.category} - ${err.message}`);
|
|
1304
|
+
failed++;
|
|
795
1305
|
}
|
|
796
1306
|
}
|
|
797
1307
|
await registry.save();
|
|
798
1308
|
blank();
|
|
799
1309
|
separator();
|
|
800
|
-
|
|
1310
|
+
if (failed > 0) {
|
|
1311
|
+
warn(`修复部分完成: 解决 ${fixed} 个问题,失败 ${failed} 项操作`);
|
|
1312
|
+
}
|
|
1313
|
+
else {
|
|
1314
|
+
success(`修复完成: ${fixed} 个问题已解决`);
|
|
1315
|
+
}
|
|
1316
|
+
debug(`[check] 修复完成: fixed=${fixed}, failed=${failed}`);
|
|
1317
|
+
return { fixed, failed };
|
|
801
1318
|
}
|
|
802
1319
|
// ============ 工具函数 ============
|
|
803
1320
|
async function getDirSizeRecursive(dirPath) {
|
|
@@ -845,6 +1362,12 @@ export async function verifyIntegrity() {
|
|
|
845
1362
|
export async function collectCheckResultForTest(options) {
|
|
846
1363
|
return collectAllIssues(options);
|
|
847
1364
|
}
|
|
1365
|
+
export async function runCheckForTest(options) {
|
|
1366
|
+
await runCheck(options);
|
|
1367
|
+
}
|
|
1368
|
+
export async function interactiveCheckForTest(result, options) {
|
|
1369
|
+
await interactiveCheck(result, options);
|
|
1370
|
+
}
|
|
848
1371
|
/**
|
|
849
1372
|
* 修复问题(兼容旧 repair 命令)
|
|
850
1373
|
*/
|
|
@@ -852,16 +1375,22 @@ export async function repairIssues(options) {
|
|
|
852
1375
|
const result = await collectAllIssues();
|
|
853
1376
|
// 渲染报告
|
|
854
1377
|
renderReport(result);
|
|
855
|
-
const hasIssues = result.summary.integrityIssues > 0;
|
|
1378
|
+
const hasIssues = result.summary.integrityIssues > 0 || hasSpaceCalibrationChoice(result.space);
|
|
856
1379
|
if (!hasIssues) {
|
|
857
1380
|
return;
|
|
858
1381
|
}
|
|
859
1382
|
if (options.dryRun) {
|
|
860
1383
|
blank();
|
|
861
|
-
hint('运行 td check --fix
|
|
1384
|
+
hint('运行 td check --fix 修复可修复问题');
|
|
862
1385
|
return;
|
|
863
1386
|
}
|
|
864
|
-
await fixAllIssues(result.integrity,
|
|
1387
|
+
await fixAllIssues(result.integrity, result.space, {
|
|
1388
|
+
force: options.force,
|
|
1389
|
+
calibrateSpace: hasSpaceCalibrationChoice(result.space),
|
|
1390
|
+
deleteUnregistered: false,
|
|
1391
|
+
verifyStoreIntegrity: false,
|
|
1392
|
+
});
|
|
1393
|
+
renderUncachedDependencyHint(result.integrity);
|
|
865
1394
|
}
|
|
866
1395
|
/**
|
|
867
1396
|
* 校验所有 Store 条目的文件完整性
|
|
@@ -902,7 +1431,7 @@ export async function checkStoreIntegrity() {
|
|
|
902
1431
|
});
|
|
903
1432
|
}
|
|
904
1433
|
}
|
|
905
|
-
if (entry.fileCount != null) {
|
|
1434
|
+
if (entry.fileCount != null && entry.contentHash) {
|
|
906
1435
|
const expectedData = {
|
|
907
1436
|
size: entry.size,
|
|
908
1437
|
fileCount: entry.fileCount,
|
|
@@ -921,22 +1450,17 @@ export async function checkStoreIntegrity() {
|
|
|
921
1450
|
}
|
|
922
1451
|
}
|
|
923
1452
|
else {
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
}
|
|
934
|
-
catch {
|
|
935
|
-
debug(`[check] 回填完整性数据失败: ${entry.libName}:${entry.commit}:${entry.platform}`);
|
|
936
|
-
}
|
|
1453
|
+
corrupted.push({
|
|
1454
|
+
libName: entry.libName,
|
|
1455
|
+
commit: entry.commit,
|
|
1456
|
+
platform: entry.platform,
|
|
1457
|
+
kind: 'missing_integrity_record',
|
|
1458
|
+
reason: 'missing integrity record',
|
|
1459
|
+
expected: { size: entry.size },
|
|
1460
|
+
actual: { fileCount: 0, size: 0 },
|
|
1461
|
+
});
|
|
937
1462
|
}
|
|
938
1463
|
}
|
|
939
|
-
await registry.save();
|
|
940
1464
|
return corrupted;
|
|
941
1465
|
}
|
|
942
1466
|
/**
|
|
@@ -948,30 +1472,42 @@ export async function checkStoreIntegrity() {
|
|
|
948
1472
|
export async function fixCorruptedStores(corrupted) {
|
|
949
1473
|
if (corrupted.length === 0)
|
|
950
1474
|
return;
|
|
1475
|
+
await withGlobalLock(() => applyCorruptedStoreFixes(corrupted));
|
|
1476
|
+
}
|
|
1477
|
+
async function applyCorruptedStoreFixes(corrupted) {
|
|
951
1478
|
const registry = getRegistry();
|
|
952
1479
|
await registry.load();
|
|
953
1480
|
const storePath = await store.getStorePath();
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1481
|
+
const spaceService = new SpaceService(registry, storePath);
|
|
1482
|
+
const executor = spaceService.createCleanupExecutor();
|
|
1483
|
+
const recovered = await executor.recoverPending();
|
|
1484
|
+
debug(`[check] 兼容修复入口开始: corruptedStores=${corrupted.length}, recovered=${recovered}`);
|
|
1485
|
+
const sharedCorruptions = corrupted.filter((item) => item.kind === 'shared_platform_conflict' || item.platform === SHARED_PLATFORM);
|
|
1486
|
+
const sharedCorruptionKeys = new Set(sharedCorruptions.map((item) => commitKey(item.libName, item.commit)));
|
|
1487
|
+
const platformCorruptions = corrupted.filter((item) => !sharedCorruptionKeys.has(commitKey(item.libName, item.commit)));
|
|
1488
|
+
for (const cleanup of [
|
|
1489
|
+
{
|
|
1490
|
+
selections: buildCorruptedSelections(sharedCorruptions, true),
|
|
1491
|
+
allowUnknownContent: false,
|
|
1492
|
+
category: 'shared-corruption',
|
|
1493
|
+
},
|
|
1494
|
+
{
|
|
1495
|
+
selections: buildCorruptedSelections(platformCorruptions, false),
|
|
1496
|
+
allowUnknownContent: false,
|
|
1497
|
+
category: 'corrupted-platform',
|
|
1498
|
+
},
|
|
1499
|
+
]) {
|
|
1500
|
+
const result = await executeRepairPlan(spaceService, executor, cleanup.selections, {
|
|
1501
|
+
requireUnreferenced: false,
|
|
1502
|
+
allowUnknownContent: cleanup.allowUnknownContent,
|
|
1503
|
+
category: cleanup.category,
|
|
1504
|
+
});
|
|
1505
|
+
for (const group of result?.succeeded ?? []) {
|
|
1506
|
+
const suffix = group.platform ? `/${group.platform}` : ' (移除整个 commit 缓存)';
|
|
1507
|
+
success(`清理损坏 Store: ${group.id.libName}/${group.id.commit.slice(0, 7)}${suffix}`);
|
|
972
1508
|
}
|
|
973
|
-
|
|
974
|
-
error(
|
|
1509
|
+
for (const failure of result?.failed ?? []) {
|
|
1510
|
+
error(`清理损坏 Store 失败: ${failure.id.libName}/${failure.id.commit.slice(0, 7)}/${failure.platform ?? 'all'} - ${failure.message}`);
|
|
975
1511
|
}
|
|
976
1512
|
}
|
|
977
1513
|
await registry.save();
|