tanmi-dock 0.10.0-beta → 1.0.4
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 +26 -5
- 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 +45 -1
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +322 -101
- 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 +8 -7
- package/dist/commands/link.d.ts.map +1 -1
- package/dist/commands/link.js +717 -221
- 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 +65 -41
- 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 +182 -40
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/unavailable.d.ts.map +1 -1
- package/dist/commands/unavailable.js +7 -2
- 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/codepac.d.ts +44 -0
- package/dist/core/codepac.d.ts.map +1 -1
- package/dist/core/codepac.js +287 -5
- package/dist/core/codepac.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 +7 -3
- package/dist/core/config.js.map +1 -1
- package/dist/core/linker.d.ts +7 -0
- package/dist/core/linker.d.ts.map +1 -1
- package/dist/core/linker.js +47 -0
- package/dist/core/linker.js.map +1 -1
- package/dist/core/parser.d.ts +30 -5
- package/dist/core/parser.d.ts.map +1 -1
- package/dist/core/parser.js +376 -57
- 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 +16 -0
- package/dist/core/space/calibration-executor.d.ts.map +1 -0
- package/dist/core/space/calibration-executor.js +142 -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 +15 -0
- package/dist/core/space/inventory.d.ts.map +1 -0
- package/dist/core/space/inventory.js +587 -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 +241 -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 +2 -0
- package/dist/core/store.d.ts.map +1 -1
- package/dist/core/store.js +44 -12
- 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 +63 -10
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.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/git.d.ts +20 -1
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +181 -4
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/logger.d.ts +1 -1
- package/dist/utils/logger.js +2 -2
- 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
|
@@ -4,12 +4,16 @@
|
|
|
4
4
|
import fs from 'fs/promises';
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import { Command } from 'commander';
|
|
7
|
-
import {
|
|
7
|
+
import { checkCodepacEnvironment } from '../core/codepac.js';
|
|
8
8
|
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';
|
|
12
|
+
import { 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';
|
|
13
17
|
import { info, warn, success, error, hint, blank, title, separator, colorize, debug, } from '../utils/logger.js';
|
|
14
18
|
import { selectWithCancel, checkboxWithCancel, confirmWithCancel, PROMPT_CANCELLED, } from '../utils/prompt.js';
|
|
15
19
|
// ============ 命令创建 ============
|
|
@@ -27,7 +31,17 @@ export function createCheckCommand() {
|
|
|
27
31
|
}
|
|
28
32
|
// ============ 主流程 ============
|
|
29
33
|
async function runCheck(options) {
|
|
30
|
-
|
|
34
|
+
if (options.dryRun) {
|
|
35
|
+
const cfg = await config.load({ migrate: false });
|
|
36
|
+
if (cfg && config.checkConfigVersion(cfg) === 'migrate') {
|
|
37
|
+
error('配置需要迁移;dry-run 不会修改配置,请先运行一次普通命令完成迁移');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const result = await collectAllIssues({
|
|
42
|
+
integrity: options.integrity,
|
|
43
|
+
readOnly: options.dryRun,
|
|
44
|
+
});
|
|
31
45
|
// JSON 输出
|
|
32
46
|
if (options.json) {
|
|
33
47
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -35,7 +49,8 @@ async function runCheck(options) {
|
|
|
35
49
|
}
|
|
36
50
|
// 渲染报告
|
|
37
51
|
renderReport(result);
|
|
38
|
-
const
|
|
52
|
+
const hasIntegrityIssues = result.summary.integrityIssues > 0;
|
|
53
|
+
const hasIssues = hasIntegrityIssues || result.summary.spaceIssues > 0;
|
|
39
54
|
const hasEnvErrors = result.summary.envErrors > 0;
|
|
40
55
|
// dry-run 模式
|
|
41
56
|
if (options.dryRun) {
|
|
@@ -55,6 +70,10 @@ async function runCheck(options) {
|
|
|
55
70
|
if (!hasIssues) {
|
|
56
71
|
return;
|
|
57
72
|
}
|
|
73
|
+
if (!hasIntegrityIssues) {
|
|
74
|
+
hint('空间差异需要通过详细检查确认,本次没有自动修改注册记录');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
58
77
|
// --fix 模式:直接修复
|
|
59
78
|
if (options.fix) {
|
|
60
79
|
await fixAllIssues(result.integrity, { force: options.force });
|
|
@@ -65,12 +84,67 @@ async function runCheck(options) {
|
|
|
65
84
|
}
|
|
66
85
|
// ============ 数据收集 ============
|
|
67
86
|
async function collectAllIssues(options) {
|
|
68
|
-
const environment = await checkEnvironment();
|
|
87
|
+
const environment = await checkEnvironment(options?.readOnly);
|
|
69
88
|
const integrity = await checkIntegrity(options);
|
|
89
|
+
const space = { issues: [] };
|
|
90
|
+
try {
|
|
91
|
+
const registry = getRegistry();
|
|
92
|
+
await registry.load({ migrate: !options?.readOnly });
|
|
93
|
+
const storePath = await store.getStorePath();
|
|
94
|
+
debug(`[space] audit 开始: store=${storePath}, source=td-check`);
|
|
95
|
+
const snapshot = await new SpaceService(registry, storePath).buildSnapshot('audit');
|
|
96
|
+
space.snapshotId = snapshot.id;
|
|
97
|
+
space.totals = snapshot.totals;
|
|
98
|
+
space.issues = snapshot.issues;
|
|
99
|
+
const cleanupLogs = await CleanupJournal.inspectPending();
|
|
100
|
+
for (const journal of cleanupLogs.journals) {
|
|
101
|
+
const data = journal.getData();
|
|
102
|
+
space.issues.push({
|
|
103
|
+
kind: 'recovery-log',
|
|
104
|
+
id: { libName: 'cleanup-journal', commit: data.planId },
|
|
105
|
+
message: `存在待恢复清理日志,组状态: ${data.groups.map((group) => `${group.index}:${group.state}`).join(', ')}`,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
for (const filePath of cleanupLogs.corruptedFiles) {
|
|
109
|
+
space.issues.push({
|
|
110
|
+
kind: 'recovery-log',
|
|
111
|
+
id: { libName: 'cleanup-journal', commit: path.basename(filePath) },
|
|
112
|
+
message: `清理日志损坏,已保留原文件: ${filePath}`,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
const migrations = await MigrationJournal.listPending();
|
|
117
|
+
for (const migration of migrations) {
|
|
118
|
+
const data = migration.getData();
|
|
119
|
+
space.issues.push({
|
|
120
|
+
kind: 'recovery-log',
|
|
121
|
+
id: { libName: 'migration-journal', commit: data.id },
|
|
122
|
+
message: `存在待恢复迁移: ${data.sourcePath} -> ${data.destinationPath},阶段 ${data.phase}`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch (migrationError) {
|
|
127
|
+
space.issues.push({
|
|
128
|
+
kind: 'recovery-log',
|
|
129
|
+
id: { libName: 'migration-journal', commit: 'corrupted' },
|
|
130
|
+
message: migrationError.message,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
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}`);
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
space.error = err.message;
|
|
137
|
+
debug(`[space] audit 失败: error=${space.error}`);
|
|
138
|
+
}
|
|
70
139
|
// 计算汇总
|
|
71
140
|
// 错误:检查项失败(ok === false),排除仅作为警告的项
|
|
72
141
|
// 警告:目前只有 disk 有 warn 字段(空间 < 5GB 但不影响功能)
|
|
73
|
-
const envErrors =
|
|
142
|
+
const envErrors = [
|
|
143
|
+
environment.codepac,
|
|
144
|
+
environment.config,
|
|
145
|
+
environment.store,
|
|
146
|
+
environment.disk,
|
|
147
|
+
].filter((c) => !c.ok).length;
|
|
74
148
|
const envWarnings = environment.disk.warn ? 1 : 0;
|
|
75
149
|
const integrityIssues = integrity.invalidProjects.length +
|
|
76
150
|
integrity.danglingLinks.length +
|
|
@@ -78,33 +152,38 @@ async function collectAllIssues(options) {
|
|
|
78
152
|
integrity.missingLibraries.length +
|
|
79
153
|
integrity.staleReferences.length +
|
|
80
154
|
integrity.corruptedStores.length;
|
|
81
|
-
const reclaimableSize =
|
|
155
|
+
const reclaimableSize = space.totals?.reclaimable.bytes
|
|
156
|
+
?? integrity.orphanLibraries.reduce((sum, lib) => sum + lib.size, 0);
|
|
82
157
|
return {
|
|
83
158
|
environment,
|
|
84
159
|
integrity,
|
|
160
|
+
space,
|
|
85
161
|
summary: {
|
|
86
162
|
envErrors,
|
|
87
163
|
envWarnings,
|
|
88
164
|
integrityIssues,
|
|
89
165
|
reclaimableSize,
|
|
166
|
+
spaceIssues: space.issues.length,
|
|
90
167
|
},
|
|
91
168
|
};
|
|
92
169
|
}
|
|
93
|
-
async function checkEnvironment() {
|
|
170
|
+
async function checkEnvironment(readOnly = false) {
|
|
171
|
+
const codepacEnvironment = await checkCodepacEnvironment();
|
|
94
172
|
const result = {
|
|
95
|
-
codepac: { ok: false, message: '' },
|
|
173
|
+
codepac: { ok: false, message: '', details: codepacEnvironment },
|
|
96
174
|
config: { ok: false, message: '' },
|
|
97
175
|
store: { ok: false, path: '', message: '' },
|
|
98
176
|
disk: { ok: false, free: 0, warn: false, message: '' },
|
|
99
177
|
};
|
|
100
|
-
|
|
101
|
-
|
|
178
|
+
debug(`[check] 环境检测开始: cwd=${process.cwd()}, codepac=${codepacEnvironment.codepacCommand.ok}, git=${codepacEnvironment.git.ok}, gitLfs=${codepacEnvironment.gitLfs.ok}, version=${codepacEnvironment.codepacVersion.ok}`);
|
|
179
|
+
// 1. codepac / Git / Git LFS
|
|
102
180
|
result.codepac = {
|
|
103
|
-
ok:
|
|
104
|
-
message:
|
|
181
|
+
ok: codepacEnvironment.ok,
|
|
182
|
+
message: formatCodepacEnvironmentMessage(codepacEnvironment),
|
|
183
|
+
details: codepacEnvironment,
|
|
105
184
|
};
|
|
106
185
|
// 2. 配置
|
|
107
|
-
const cfg = await config.load();
|
|
186
|
+
const cfg = await config.load({ migrate: !readOnly });
|
|
108
187
|
result.config = {
|
|
109
188
|
ok: !!cfg,
|
|
110
189
|
message: cfg ? '已初始化' : '未初始化,运行 td init',
|
|
@@ -155,6 +234,7 @@ async function checkEnvironment() {
|
|
|
155
234
|
message: '无法获取磁盘信息',
|
|
156
235
|
};
|
|
157
236
|
}
|
|
237
|
+
debug(`[check] 环境检测完成: codepacOk=${result.codepac.ok}, configOk=${result.config.ok}, storeOk=${result.store.ok}, diskOk=${result.disk.ok}, diskWarn=${result.disk.warn}`);
|
|
158
238
|
return result;
|
|
159
239
|
}
|
|
160
240
|
async function checkIntegrity(options) {
|
|
@@ -171,7 +251,7 @@ async function checkIntegrity(options) {
|
|
|
171
251
|
let storePath;
|
|
172
252
|
try {
|
|
173
253
|
registry = getRegistry();
|
|
174
|
-
await registry.load();
|
|
254
|
+
await registry.load({ migrate: !options?.readOnly });
|
|
175
255
|
storePath = await store.getStorePath();
|
|
176
256
|
}
|
|
177
257
|
catch (err) {
|
|
@@ -340,7 +420,7 @@ async function checkIntegrity(options) {
|
|
|
340
420
|
if (options?.integrity) {
|
|
341
421
|
const stores = registry.listStores();
|
|
342
422
|
for (const entry of stores) {
|
|
343
|
-
if (entry.fileCount != null) {
|
|
423
|
+
if (entry.fileCount != null && entry.contentHash) {
|
|
344
424
|
// 有 fileCount 的做 fullVerify
|
|
345
425
|
const expectedData = {
|
|
346
426
|
size: entry.size,
|
|
@@ -360,23 +440,17 @@ async function checkIntegrity(options) {
|
|
|
360
440
|
}
|
|
361
441
|
}
|
|
362
442
|
else {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
}
|
|
373
|
-
catch {
|
|
374
|
-
debug(`[check] 回填完整性数据失败: ${entry.libName}:${entry.commit}:${entry.platform}`);
|
|
375
|
-
}
|
|
443
|
+
result.corruptedStores.push({
|
|
444
|
+
libName: entry.libName,
|
|
445
|
+
commit: entry.commit,
|
|
446
|
+
platform: entry.platform,
|
|
447
|
+
kind: 'missing_integrity_record',
|
|
448
|
+
reason: 'missing integrity record',
|
|
449
|
+
expected: { size: entry.size },
|
|
450
|
+
actual: { fileCount: 0, size: 0 },
|
|
451
|
+
});
|
|
376
452
|
}
|
|
377
453
|
}
|
|
378
|
-
// 保存回填的数据
|
|
379
|
-
await registry.save();
|
|
380
454
|
}
|
|
381
455
|
return result;
|
|
382
456
|
}
|
|
@@ -389,6 +463,7 @@ function renderReport(result) {
|
|
|
389
463
|
separator();
|
|
390
464
|
const env = result.environment;
|
|
391
465
|
renderCheck('codepac', env.codepac.ok, env.codepac.message, false);
|
|
466
|
+
renderCodepacEnvironmentDetails(env.codepac.details);
|
|
392
467
|
renderCheck('配置文件', env.config.ok, env.config.message, false);
|
|
393
468
|
renderCheck('Store 目录', env.store.ok, env.store.message, false);
|
|
394
469
|
renderCheck('磁盘空间', env.disk.ok, env.disk.message, env.disk.warn);
|
|
@@ -417,10 +492,28 @@ function renderReport(result) {
|
|
|
417
492
|
? '完整'
|
|
418
493
|
: `${integ.corruptedStores.length} 个损坏的 Store 条目`, false);
|
|
419
494
|
blank();
|
|
495
|
+
info('空间一致性');
|
|
496
|
+
separator();
|
|
497
|
+
if (result.space.error) {
|
|
498
|
+
renderCheck('空间审计', false, result.space.error, false);
|
|
499
|
+
}
|
|
500
|
+
else if (result.space.totals) {
|
|
501
|
+
const totals = result.space.totals;
|
|
502
|
+
renderCheck('空间审计', result.space.issues.length === 0, result.space.issues.length === 0
|
|
503
|
+
? `实际占用 ${formatSize(totals.physical.bytes)},登记容量一致`
|
|
504
|
+
: `${result.space.issues.length} 个差异,实际 ${formatSize(totals.physical.bytes)},登记 ${formatSize(totals.registered.bytes)}`, false);
|
|
505
|
+
for (const issue of result.space.issues.slice(0, 20)) {
|
|
506
|
+
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}`);
|
|
507
|
+
}
|
|
508
|
+
if (result.space.issues.length > 20) {
|
|
509
|
+
debug(`[space] audit 差异省略: remaining=${result.space.issues.length - 20}`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
blank();
|
|
420
513
|
separator();
|
|
421
514
|
// 汇总
|
|
422
|
-
const { envErrors, envWarnings, integrityIssues, reclaimableSize } = result.summary;
|
|
423
|
-
if (envErrors === 0 && envWarnings === 0 && integrityIssues === 0) {
|
|
515
|
+
const { envErrors, envWarnings, integrityIssues, reclaimableSize, spaceIssues } = result.summary;
|
|
516
|
+
if (envErrors === 0 && envWarnings === 0 && integrityIssues === 0 && spaceIssues === 0) {
|
|
424
517
|
success('系统健康,无问题');
|
|
425
518
|
}
|
|
426
519
|
else {
|
|
@@ -431,11 +524,55 @@ function renderReport(result) {
|
|
|
431
524
|
parts.push(`${envWarnings} 个警告`);
|
|
432
525
|
if (integrityIssues > 0)
|
|
433
526
|
parts.push(`${integrityIssues} 个数据问题`);
|
|
527
|
+
if (spaceIssues > 0)
|
|
528
|
+
parts.push(`${spaceIssues} 个空间差异`);
|
|
434
529
|
if (reclaimableSize > 0)
|
|
435
530
|
parts.push(`可回收 ${formatSize(reclaimableSize)}`);
|
|
436
531
|
warn(`发现问题: ${parts.join(', ')}`);
|
|
437
532
|
}
|
|
438
533
|
}
|
|
534
|
+
function formatCodepacEnvironmentMessage(environment) {
|
|
535
|
+
if (environment.ok) {
|
|
536
|
+
return environment.codepacVersion.version || 'CodePac、Git、Git LFS 可用';
|
|
537
|
+
}
|
|
538
|
+
const failures = [];
|
|
539
|
+
if (!environment.codepacCommand.ok) {
|
|
540
|
+
failures.push('CodePac 命令缺失');
|
|
541
|
+
}
|
|
542
|
+
if (!environment.git.ok) {
|
|
543
|
+
failures.push(environment.git.version
|
|
544
|
+
? `Git 版本不足(${environment.git.version} < ${environment.git.minimumVersion})`
|
|
545
|
+
: 'Git 不可用');
|
|
546
|
+
}
|
|
547
|
+
if (!environment.gitLfs.ok) {
|
|
548
|
+
failures.push('Git LFS 不可用');
|
|
549
|
+
}
|
|
550
|
+
if (environment.codepacCommand.ok && !environment.codepacVersion.ok) {
|
|
551
|
+
failures.push('codepac --version 执行失败');
|
|
552
|
+
}
|
|
553
|
+
return failures.join(';') || 'CodePac 环境不可用';
|
|
554
|
+
}
|
|
555
|
+
function renderCodepacEnvironmentDetails(environment) {
|
|
556
|
+
debug(`[check] CodePac 环境详情: commandOk=${environment.codepacCommand.ok}, gitVersion=${environment.git.version ?? 'unknown'}, gitOk=${environment.git.ok}, gitLfsOk=${environment.gitLfs.ok}, versionOk=${environment.codepacVersion.ok}`);
|
|
557
|
+
if (environment.ok) {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (!environment.codepacCommand.ok) {
|
|
561
|
+
warn(` - CodePac 命令: ${environment.codepacCommand.message}`);
|
|
562
|
+
}
|
|
563
|
+
if (!environment.git.ok) {
|
|
564
|
+
warn(` - Git: ${environment.git.message}`);
|
|
565
|
+
}
|
|
566
|
+
if (!environment.gitLfs.ok) {
|
|
567
|
+
warn(` - Git LFS: ${environment.gitLfs.message}`);
|
|
568
|
+
}
|
|
569
|
+
if (environment.codepacCommand.ok && !environment.codepacVersion.ok) {
|
|
570
|
+
warn(` - CodePac 版本命令: ${environment.codepacVersion.message}`);
|
|
571
|
+
if (environment.codepacVersion.error) {
|
|
572
|
+
debug(`[check] codepac --version 错误: ${environment.codepacVersion.error}`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
439
576
|
function renderCheck(name, ok, message, isWarning) {
|
|
440
577
|
if (ok && !isWarning) {
|
|
441
578
|
success(`[✓] ${name.padEnd(10)} ${message}`);
|
|
@@ -645,15 +782,68 @@ async function fixAllIssues(issues, options) {
|
|
|
645
782
|
separator();
|
|
646
783
|
info('正在修复...');
|
|
647
784
|
blank();
|
|
785
|
+
await withGlobalLock(() => applyAllIssueFixes(issues));
|
|
786
|
+
}
|
|
787
|
+
function commitKey(libName, commit) {
|
|
788
|
+
return JSON.stringify([libName, commit]);
|
|
789
|
+
}
|
|
790
|
+
function buildCorruptedSelections(corrupted, includeShared) {
|
|
791
|
+
const grouped = new Map();
|
|
792
|
+
for (const item of corrupted) {
|
|
793
|
+
const key = commitKey(item.libName, item.commit);
|
|
794
|
+
const group = grouped.get(key) ?? {
|
|
795
|
+
id: { libName: item.libName, commit: item.commit },
|
|
796
|
+
platforms: new Set(),
|
|
797
|
+
};
|
|
798
|
+
if (!includeShared)
|
|
799
|
+
group.platforms.add(item.platform);
|
|
800
|
+
grouped.set(key, group);
|
|
801
|
+
}
|
|
802
|
+
return [...grouped.values()].map((group) => ({
|
|
803
|
+
id: group.id,
|
|
804
|
+
platforms: includeShared ? 'all' : [...group.platforms],
|
|
805
|
+
...(includeShared ? { includeShared: true } : {}),
|
|
806
|
+
}));
|
|
807
|
+
}
|
|
808
|
+
async function executeRepairPlan(spaceService, executor, selections, options) {
|
|
809
|
+
if (selections.length === 0)
|
|
810
|
+
return null;
|
|
811
|
+
const plan = await spaceService.buildCleanupPlan({
|
|
812
|
+
reason: 'repair',
|
|
813
|
+
selections,
|
|
814
|
+
requireUnreferenced: options.requireUnreferenced,
|
|
815
|
+
allowUnknownContent: options.allowUnknownContent,
|
|
816
|
+
});
|
|
817
|
+
for (const warningMessage of plan.warnings)
|
|
818
|
+
warn(`[space] ${warningMessage}`);
|
|
819
|
+
debug(`[check] 调用统一清理执行器: category=${options.category}, plan=${plan.id}, selections=${selections.length}, groups=${plan.groups.length}, expected=${plan.expectedRelease.bytes}`);
|
|
820
|
+
if (plan.groups.length === 0)
|
|
821
|
+
return null;
|
|
822
|
+
const result = await executor.execute(plan);
|
|
823
|
+
debug(`[check] 清理执行返回: category=${options.category}, plan=${plan.id}, status=${result.status}, succeeded=${result.succeeded.length}, failed=${result.failed.length}, released=${result.released.bytes}`);
|
|
824
|
+
return result;
|
|
825
|
+
}
|
|
826
|
+
function resolvedIssueCount(result, issues) {
|
|
827
|
+
if (!result)
|
|
828
|
+
return 0;
|
|
829
|
+
return issues.filter((issue) => result.succeeded.some((group) => group.id.libName === issue.libName
|
|
830
|
+
&& group.id.commit === issue.commit
|
|
831
|
+
&& (group.platform == null || issue.platform == null || group.platform === issue.platform))).length;
|
|
832
|
+
}
|
|
833
|
+
async function applyAllIssueFixes(issues) {
|
|
648
834
|
const registry = getRegistry();
|
|
649
835
|
await registry.load();
|
|
650
836
|
const storePath = await store.getStorePath();
|
|
837
|
+
const spaceService = new SpaceService(registry, storePath);
|
|
838
|
+
const executor = spaceService.createCleanupExecutor();
|
|
839
|
+
const recovered = await executor.recoverPending();
|
|
840
|
+
debug(`[check] 修复开始: invalidProjects=${issues.invalidProjects.length}, danglingLinks=${issues.danglingLinks.length}, orphanLibraries=${issues.orphanLibraries.length}, staleReferences=${issues.staleReferences.length}, corruptedStores=${issues.corruptedStores.length}, recovered=${recovered}`);
|
|
651
841
|
let fixed = 0;
|
|
652
842
|
// 1. 清理无效项目
|
|
653
843
|
for (const p of issues.invalidProjects) {
|
|
654
844
|
try {
|
|
655
845
|
registry.removeProject(p.hash);
|
|
656
|
-
success(
|
|
846
|
+
success(`清理项目: ${p.path}`);
|
|
657
847
|
fixed++;
|
|
658
848
|
}
|
|
659
849
|
catch (err) {
|
|
@@ -678,23 +868,28 @@ async function fixAllIssues(issues, options) {
|
|
|
678
868
|
error(`[err] 移除链接失败: ${link.path} - ${err.message}`);
|
|
679
869
|
}
|
|
680
870
|
}
|
|
681
|
-
// 3.
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
871
|
+
// 3. 删除孤立库。显式修复允许清理该版本中的未登记内容。
|
|
872
|
+
const orphanSelections = [...new Map(issues.orphanLibraries.map((lib) => [
|
|
873
|
+
commitKey(lib.libName, lib.commit),
|
|
874
|
+
{ id: { libName: lib.libName, commit: lib.commit }, platforms: 'all' },
|
|
875
|
+
])).values()];
|
|
876
|
+
try {
|
|
877
|
+
const result = await executeRepairPlan(spaceService, executor, orphanSelections, {
|
|
878
|
+
requireUnreferenced: true,
|
|
879
|
+
allowUnknownContent: true,
|
|
880
|
+
category: 'orphan-library',
|
|
881
|
+
});
|
|
882
|
+
fixed += resolvedIssueCount(result, issues.orphanLibraries);
|
|
883
|
+
for (const group of result?.succeeded ?? []) {
|
|
884
|
+
success(`[ok] 删除孤立库: ${group.id.libName}/${group.id.commit.slice(0, 7)}`);
|
|
693
885
|
}
|
|
694
|
-
|
|
695
|
-
error(`[err] 删除孤立库失败: ${
|
|
886
|
+
for (const failure of result?.failed ?? []) {
|
|
887
|
+
error(`[err] 删除孤立库失败: ${failure.id.libName}/${failure.id.commit.slice(0, 7)} - ${failure.message}`);
|
|
696
888
|
}
|
|
697
889
|
}
|
|
890
|
+
catch (err) {
|
|
891
|
+
error(`[err] 删除孤立库失败: ${err.message}`);
|
|
892
|
+
}
|
|
698
893
|
// 4. 移除失效引用
|
|
699
894
|
for (const ref of issues.staleReferences) {
|
|
700
895
|
try {
|
|
@@ -718,35 +913,51 @@ async function fixAllIssues(issues, options) {
|
|
|
718
913
|
error(`[err] 移除引用失败: ${ref.libKey} - ${err.message}`);
|
|
719
914
|
}
|
|
720
915
|
}
|
|
721
|
-
// 5. 清理损坏的 Store
|
|
722
|
-
|
|
916
|
+
// 5. 清理损坏的 Store 条目。共享冲突整版本处理,其他问题按平台处理。
|
|
917
|
+
const orphanKeys = new Set(issues.orphanLibraries.map((item) => commitKey(item.libName, item.commit)));
|
|
918
|
+
const sharedCorruptions = issues.corruptedStores.filter((item) => (item.kind === 'shared_platform_conflict' || item.platform === SHARED_PLATFORM)
|
|
919
|
+
&& !orphanKeys.has(commitKey(item.libName, item.commit)));
|
|
920
|
+
const sharedCorruptionKeys = new Set(sharedCorruptions.map((item) => commitKey(item.libName, item.commit)));
|
|
921
|
+
const platformCorruptions = issues.corruptedStores.filter((item) => !orphanKeys.has(commitKey(item.libName, item.commit))
|
|
922
|
+
&& !sharedCorruptionKeys.has(commitKey(item.libName, item.commit)));
|
|
923
|
+
for (const cleanup of [
|
|
924
|
+
{
|
|
925
|
+
items: issues.corruptedStores.filter((item) => sharedCorruptionKeys.has(commitKey(item.libName, item.commit))),
|
|
926
|
+
selections: buildCorruptedSelections(sharedCorruptions, true),
|
|
927
|
+
allowUnknownContent: false,
|
|
928
|
+
category: 'shared-corruption',
|
|
929
|
+
},
|
|
930
|
+
{
|
|
931
|
+
items: platformCorruptions,
|
|
932
|
+
selections: buildCorruptedSelections(platformCorruptions, false),
|
|
933
|
+
allowUnknownContent: false,
|
|
934
|
+
category: 'corrupted-platform',
|
|
935
|
+
},
|
|
936
|
+
]) {
|
|
723
937
|
try {
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
938
|
+
const result = await executeRepairPlan(spaceService, executor, cleanup.selections, {
|
|
939
|
+
requireUnreferenced: false,
|
|
940
|
+
allowUnknownContent: cleanup.allowUnknownContent,
|
|
941
|
+
category: cleanup.category,
|
|
942
|
+
});
|
|
943
|
+
fixed += resolvedIssueCount(result, cleanup.items);
|
|
944
|
+
for (const group of result?.succeeded ?? []) {
|
|
945
|
+
const suffix = group.platform ? `/${group.platform}` : ' (移除整个 commit 缓存)';
|
|
946
|
+
success(`清理损坏 Store: ${group.id.libName}/${group.id.commit.slice(0, 7)}${suffix}`);
|
|
732
947
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
registry.removeStore(storeKey);
|
|
736
|
-
const platformPath = path.join(storePath, item.libName, item.commit, item.platform);
|
|
737
|
-
await fs.rm(platformPath, { recursive: true, force: true });
|
|
738
|
-
success(`[ok] 清理损坏 Store: ${item.libName}/${item.commit.slice(0, 7)}/${item.platform}`);
|
|
948
|
+
for (const failure of result?.failed ?? []) {
|
|
949
|
+
error(`清理损坏 Store 失败: ${failure.id.libName}/${failure.id.commit.slice(0, 7)}/${failure.platform ?? 'all'} - ${failure.message}`);
|
|
739
950
|
}
|
|
740
|
-
fixed++;
|
|
741
951
|
}
|
|
742
952
|
catch (err) {
|
|
743
|
-
error(
|
|
953
|
+
error(`清理损坏 Store 失败: category=${cleanup.category} - ${err.message}`);
|
|
744
954
|
}
|
|
745
955
|
}
|
|
746
956
|
await registry.save();
|
|
747
957
|
blank();
|
|
748
958
|
separator();
|
|
749
959
|
success(`修复完成: ${fixed} 个问题已解决`);
|
|
960
|
+
debug(`[check] 修复完成: fixed=${fixed}`);
|
|
750
961
|
}
|
|
751
962
|
// ============ 工具函数 ============
|
|
752
963
|
async function getDirSizeRecursive(dirPath) {
|
|
@@ -791,6 +1002,9 @@ export async function verifyIntegrity() {
|
|
|
791
1002
|
const result = await collectAllIssues();
|
|
792
1003
|
renderReport(result);
|
|
793
1004
|
}
|
|
1005
|
+
export async function collectCheckResultForTest(options) {
|
|
1006
|
+
return collectAllIssues(options);
|
|
1007
|
+
}
|
|
794
1008
|
/**
|
|
795
1009
|
* 修复问题(兼容旧 repair 命令)
|
|
796
1010
|
*/
|
|
@@ -848,7 +1062,7 @@ export async function checkStoreIntegrity() {
|
|
|
848
1062
|
});
|
|
849
1063
|
}
|
|
850
1064
|
}
|
|
851
|
-
if (entry.fileCount != null) {
|
|
1065
|
+
if (entry.fileCount != null && entry.contentHash) {
|
|
852
1066
|
const expectedData = {
|
|
853
1067
|
size: entry.size,
|
|
854
1068
|
fileCount: entry.fileCount,
|
|
@@ -867,22 +1081,17 @@ export async function checkStoreIntegrity() {
|
|
|
867
1081
|
}
|
|
868
1082
|
}
|
|
869
1083
|
else {
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
}
|
|
880
|
-
catch {
|
|
881
|
-
debug(`[check] 回填完整性数据失败: ${entry.libName}:${entry.commit}:${entry.platform}`);
|
|
882
|
-
}
|
|
1084
|
+
corrupted.push({
|
|
1085
|
+
libName: entry.libName,
|
|
1086
|
+
commit: entry.commit,
|
|
1087
|
+
platform: entry.platform,
|
|
1088
|
+
kind: 'missing_integrity_record',
|
|
1089
|
+
reason: 'missing integrity record',
|
|
1090
|
+
expected: { size: entry.size },
|
|
1091
|
+
actual: { fileCount: 0, size: 0 },
|
|
1092
|
+
});
|
|
883
1093
|
}
|
|
884
1094
|
}
|
|
885
|
-
await registry.save();
|
|
886
1095
|
return corrupted;
|
|
887
1096
|
}
|
|
888
1097
|
/**
|
|
@@ -894,30 +1103,42 @@ export async function checkStoreIntegrity() {
|
|
|
894
1103
|
export async function fixCorruptedStores(corrupted) {
|
|
895
1104
|
if (corrupted.length === 0)
|
|
896
1105
|
return;
|
|
1106
|
+
await withGlobalLock(() => applyCorruptedStoreFixes(corrupted));
|
|
1107
|
+
}
|
|
1108
|
+
async function applyCorruptedStoreFixes(corrupted) {
|
|
897
1109
|
const registry = getRegistry();
|
|
898
1110
|
await registry.load();
|
|
899
1111
|
const storePath = await store.getStorePath();
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1112
|
+
const spaceService = new SpaceService(registry, storePath);
|
|
1113
|
+
const executor = spaceService.createCleanupExecutor();
|
|
1114
|
+
const recovered = await executor.recoverPending();
|
|
1115
|
+
debug(`[check] 兼容修复入口开始: corruptedStores=${corrupted.length}, recovered=${recovered}`);
|
|
1116
|
+
const sharedCorruptions = corrupted.filter((item) => item.kind === 'shared_platform_conflict' || item.platform === SHARED_PLATFORM);
|
|
1117
|
+
const sharedCorruptionKeys = new Set(sharedCorruptions.map((item) => commitKey(item.libName, item.commit)));
|
|
1118
|
+
const platformCorruptions = corrupted.filter((item) => !sharedCorruptionKeys.has(commitKey(item.libName, item.commit)));
|
|
1119
|
+
for (const cleanup of [
|
|
1120
|
+
{
|
|
1121
|
+
selections: buildCorruptedSelections(sharedCorruptions, true),
|
|
1122
|
+
allowUnknownContent: false,
|
|
1123
|
+
category: 'shared-corruption',
|
|
1124
|
+
},
|
|
1125
|
+
{
|
|
1126
|
+
selections: buildCorruptedSelections(platformCorruptions, false),
|
|
1127
|
+
allowUnknownContent: false,
|
|
1128
|
+
category: 'corrupted-platform',
|
|
1129
|
+
},
|
|
1130
|
+
]) {
|
|
1131
|
+
const result = await executeRepairPlan(spaceService, executor, cleanup.selections, {
|
|
1132
|
+
requireUnreferenced: false,
|
|
1133
|
+
allowUnknownContent: cleanup.allowUnknownContent,
|
|
1134
|
+
category: cleanup.category,
|
|
1135
|
+
});
|
|
1136
|
+
for (const group of result?.succeeded ?? []) {
|
|
1137
|
+
const suffix = group.platform ? `/${group.platform}` : ' (移除整个 commit 缓存)';
|
|
1138
|
+
success(`清理损坏 Store: ${group.id.libName}/${group.id.commit.slice(0, 7)}${suffix}`);
|
|
918
1139
|
}
|
|
919
|
-
|
|
920
|
-
error(
|
|
1140
|
+
for (const failure of result?.failed ?? []) {
|
|
1141
|
+
error(`清理损坏 Store 失败: ${failure.id.libName}/${failure.id.commit.slice(0, 7)}/${failure.platform ?? 'all'} - ${failure.message}`);
|
|
921
1142
|
}
|
|
922
1143
|
}
|
|
923
1144
|
await registry.save();
|