tanmi-dock 1.0.4 → 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.
@@ -9,18 +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, SHARED_PLATFORM } from '../core/platform.js';
12
+ import { GENERAL_PLATFORM, KNOWN_PLATFORM_VALUES, SHARED_PLATFORM } from '../core/platform.js';
13
13
  import { SpaceService } from '../core/space/service.js';
14
14
  import { CleanupJournal } from '../core/space/cleanup-journal.js';
15
15
  import { MigrationJournal } from '../core/space/migration-journal.js';
16
16
  import { withGlobalLock } from '../utils/global-lock.js';
17
- import { info, warn, success, error, hint, blank, title, separator, colorize, debug, } from '../utils/logger.js';
17
+ import { info, warn, success, error, hint, blank, title, separator, colorize, debug, withStdoutLogsSuppressed, } from '../utils/logger.js';
18
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
+ }
19
92
  // ============ 命令创建 ============
20
93
  export function createCheckCommand() {
21
94
  return new Command('check')
22
95
  .description('健康检查(环境诊断 + 数据一致性验证)')
23
- .option('--fix', '直接修复所有问题')
96
+ .option('--fix', '直接修复所有可修复问题')
24
97
  .option('--dry-run', '只显示问题,不修复')
25
98
  .option('--json', '输出 JSON 格式')
26
99
  .option('--force', '跳过确认')
@@ -31,6 +104,14 @@ export function createCheckCommand() {
31
104
  }
32
105
  // ============ 主流程 ============
33
106
  async function runCheck(options) {
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}`);
34
115
  if (options.dryRun) {
35
116
  const cfg = await config.load({ migrate: false });
36
117
  if (cfg && config.checkConfigVersion(cfg) === 'migrate') {
@@ -41,6 +122,7 @@ async function runCheck(options) {
41
122
  const result = await collectAllIssues({
42
123
  integrity: options.integrity,
43
124
  readOnly: options.dryRun,
125
+ onProgress: options.json ? undefined : createCheckProgressRenderer(),
44
126
  });
45
127
  // JSON 输出
46
128
  if (options.json) {
@@ -50,13 +132,14 @@ async function runCheck(options) {
50
132
  // 渲染报告
51
133
  renderReport(result);
52
134
  const hasIntegrityIssues = result.summary.integrityIssues > 0;
53
- const hasIssues = hasIntegrityIssues || result.summary.spaceIssues > 0;
135
+ const hasReportedIssues = hasIntegrityIssues || result.summary.spaceIssues > 0;
136
+ const hasRepairableIssues = hasIntegrityIssues || hasSpaceCalibrationChoice(result.space);
54
137
  const hasEnvErrors = result.summary.envErrors > 0;
55
138
  // dry-run 模式
56
139
  if (options.dryRun) {
57
- if (hasIssues) {
140
+ if (hasRepairableIssues) {
58
141
  blank();
59
- hint('运行 td check --fix 修复问题');
142
+ hint('运行 td check --fix 修复可修复问题');
60
143
  }
61
144
  return;
62
145
  }
@@ -67,35 +150,48 @@ async function runCheck(options) {
67
150
  return;
68
151
  }
69
152
  // 无问题
70
- if (!hasIssues) {
71
- return;
72
- }
73
- if (!hasIntegrityIssues) {
74
- hint('空间差异需要通过详细检查确认,本次没有自动修改注册记录');
153
+ if (!hasRepairableIssues) {
154
+ if (hasReportedIssues) {
155
+ debug('[check] 检查结果只包含无自动修复动作的问题');
156
+ }
75
157
  return;
76
158
  }
77
159
  // --fix 模式:直接修复
78
160
  if (options.fix) {
79
- await fixAllIssues(result.integrity, { force: options.force });
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);
80
168
  return;
81
169
  }
82
170
  // 交互模式
83
- await interactiveCheck(result.integrity, options);
171
+ await interactiveCheck(result, options);
84
172
  }
85
173
  // ============ 数据收集 ============
86
- async function collectAllIssues(options) {
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' });
87
177
  const environment = await checkEnvironment(options?.readOnly);
178
+ options.onProgress?.({ stage: 'environment', state: 'completed' });
179
+ options.onProgress?.({ stage: 'integrity', state: 'started' });
88
180
  const integrity = await checkIntegrity(options);
181
+ options.onProgress?.({ stage: 'integrity', state: 'completed' });
89
182
  const space = { issues: [] };
183
+ options.onProgress?.({ stage: 'space', state: 'started' });
90
184
  try {
91
185
  const registry = getRegistry();
92
186
  await registry.load({ migrate: !options?.readOnly });
93
187
  const storePath = await store.getStorePath();
94
188
  debug(`[space] audit 开始: store=${storePath}, source=td-check`);
95
- const snapshot = await new SpaceService(registry, storePath).buildSnapshot('audit');
189
+ const spaceService = new SpaceService(registry, storePath);
190
+ const snapshot = await spaceService.buildSnapshot('audit', undefined, (progress) => options.onProgress?.({ stage: 'space', state: 'progress', progress }));
96
191
  space.snapshotId = snapshot.id;
97
192
  space.totals = snapshot.totals;
98
193
  space.issues = snapshot.issues;
194
+ space.calibrationPlan = spaceService.buildCalibrationPlan(snapshot);
99
195
  const cleanupLogs = await CleanupJournal.inspectPending();
100
196
  for (const journal of cleanupLogs.journals) {
101
197
  const data = journal.getData();
@@ -131,10 +227,12 @@ async function collectAllIssues(options) {
131
227
  });
132
228
  }
133
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' });
134
231
  }
135
232
  catch (err) {
136
233
  space.error = err.message;
137
234
  debug(`[space] audit 失败: error=${space.error}`);
235
+ options.onProgress?.({ stage: 'space', state: 'failed', error: space.error });
138
236
  }
139
237
  // 计算汇总
140
238
  // 错误:检查项失败(ok === false),排除仅作为警告的项
@@ -149,12 +247,11 @@ async function collectAllIssues(options) {
149
247
  const integrityIssues = integrity.invalidProjects.length +
150
248
  integrity.danglingLinks.length +
151
249
  integrity.orphanLibraries.length +
152
- integrity.missingLibraries.length +
153
250
  integrity.staleReferences.length +
154
251
  integrity.corruptedStores.length;
155
252
  const reclaimableSize = space.totals?.reclaimable.bytes
156
253
  ?? integrity.orphanLibraries.reduce((sum, lib) => sum + lib.size, 0);
157
- return {
254
+ const result = {
158
255
  environment,
159
256
  integrity,
160
257
  space,
@@ -162,10 +259,13 @@ async function collectAllIssues(options) {
162
259
  envErrors,
163
260
  envWarnings,
164
261
  integrityIssues,
262
+ uncachedDependencies: integrity.missingLibraries.length,
165
263
  reclaimableSize,
166
264
  spaceIssues: space.issues.length,
167
265
  },
168
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;
169
269
  }
170
270
  async function checkEnvironment(readOnly = false) {
171
271
  const codepacEnvironment = await checkCodepacEnvironment();
@@ -260,6 +360,7 @@ async function checkIntegrity(options) {
260
360
  return result;
261
361
  }
262
362
  const projects = registry.listProjects();
363
+ debug(`[check] 数据一致性检查开始: store=${storePath}, projects=${projects.length}, integrity=${options?.integrity ?? false}, readOnly=${options?.readOnly ?? false}`);
263
364
  // 1. 检查项目和链接
264
365
  for (const project of projects) {
265
366
  const projectHash = registry.hashPath(project.path);
@@ -279,6 +380,25 @@ async function checkIntegrity(options) {
279
380
  if (!verifyPlatform) {
280
381
  continue;
281
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
+ }
282
402
  try {
283
403
  const stat = await fs.lstat(linkPath);
284
404
  if (stat.isSymbolicLink()) {
@@ -291,21 +411,19 @@ async function checkIntegrity(options) {
291
411
  result.danglingLinks.push({
292
412
  path: linkPath,
293
413
  projectHash,
294
- dep: { libName: dep.libName, commit: dep.commit },
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
+ },
295
421
  });
296
422
  }
297
423
  }
298
424
  }
299
425
  catch {
300
- // 链接不存在,检查库是否在 Store
301
- const exists = await store.exists(dep.libName, dep.commit, verifyPlatform);
302
- if (!exists) {
303
- result.missingLibraries.push({
304
- libName: dep.libName,
305
- commit: dep.commit,
306
- project: project.path,
307
- });
308
- }
426
+ // Store 缓存存在但项目链接缺失,后续引用检查会报告失效引用。
309
427
  }
310
428
  }
311
429
  }
@@ -452,6 +570,7 @@ async function checkIntegrity(options) {
452
570
  }
453
571
  }
454
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}`);
455
574
  return result;
456
575
  }
457
576
  // ============ 报告渲染 ============
@@ -481,10 +600,9 @@ function renderReport(result) {
481
600
  renderCheck('孤立库', integ.orphanLibraries.length === 0, integ.orphanLibraries.length === 0
482
601
  ? '无'
483
602
  : `${integ.orphanLibraries.length} 个未登记 (${formatSize(result.summary.reclaimableSize)})`, false);
484
- renderCheck('缺失库', integ.missingLibraries.length === 0, integ.missingLibraries.length === 0
603
+ renderCheck('本地未缓存依赖', integ.missingLibraries.length === 0, integ.missingLibraries.length === 0
485
604
  ? '无'
486
- : `${integ.missingLibraries.length} (需 td link 下载)`, true // 显示为警告而非错误,因为需要用户手动下载
487
- );
605
+ : `${integ.missingLibraries.length} 项(项目需要时可运行 td link)`, integ.missingLibraries.length > 0);
488
606
  renderCheck('引用关系', integ.staleReferences.length === 0, integ.staleReferences.length === 0
489
607
  ? '一致'
490
608
  : `${integ.staleReferences.length} 个失效引用`, false);
@@ -512,8 +630,12 @@ function renderReport(result) {
512
630
  blank();
513
631
  separator();
514
632
  // 汇总
515
- const { envErrors, envWarnings, integrityIssues, reclaimableSize, spaceIssues } = result.summary;
516
- if (envErrors === 0 && envWarnings === 0 && integrityIssues === 0 && spaceIssues === 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) {
517
639
  success('系统健康,无问题');
518
640
  }
519
641
  else {
@@ -524,6 +646,8 @@ function renderReport(result) {
524
646
  parts.push(`${envWarnings} 个警告`);
525
647
  if (integrityIssues > 0)
526
648
  parts.push(`${integrityIssues} 个数据问题`);
649
+ if (uncachedDependencies > 0)
650
+ parts.push(`${uncachedDependencies} 项本地未缓存依赖`);
527
651
  if (spaceIssues > 0)
528
652
  parts.push(`${spaceIssues} 个空间差异`);
529
653
  if (reclaimableSize > 0)
@@ -585,43 +709,55 @@ function renderCheck(name, ok, message, isWarning) {
585
709
  }
586
710
  }
587
711
  // ============ 交互式修复 ============
588
- async function interactiveCheck(issues, options) {
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;
589
719
  blank();
590
- const action = await selectWithCancel({
591
- message: '选择操作',
592
- choices: [
593
- { name: '修复所有问题', value: 'fix-all' },
594
- { name: '选择性修复', value: 'select' },
595
- { name: '查看详情', value: 'detail' },
596
- { name: colorize('← 退出', 'dim'), value: 'exit' },
597
- ],
598
- });
599
- // ESC 取消 = 退出
600
- if (action === PROMPT_CANCELLED) {
601
- info('已取消');
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);
602
756
  return;
603
757
  }
604
- switch (action) {
605
- case 'fix-all':
606
- await fixAllIssues(issues, { force: true });
607
- // 缺失库无法自动修复,需要提示用户
608
- if (issues.missingLibraries.length > 0) {
609
- blank();
610
- hint(`缺失库 (${issues.missingLibraries.length} 个) 需要通过 td link 重新下载`);
611
- }
612
- break;
613
- case 'select':
614
- await selectiveFix(issues, options);
615
- break;
616
- case 'detail':
617
- showDetails(issues);
618
- await interactiveCheck(issues, options);
619
- break;
620
- case 'exit':
621
- break;
622
- }
623
758
  }
624
- async function selectiveFix(issues, options) {
759
+ async function selectiveFix(result, options) {
760
+ const issues = result.integrity;
625
761
  // 构建选项
626
762
  const choices = [];
627
763
  if (issues.invalidProjects.length > 0) {
@@ -660,6 +796,13 @@ async function selectiveFix(issues, options) {
660
796
  checked: true,
661
797
  });
662
798
  }
799
+ if (hasSpaceCalibrationChoice(result.space)) {
800
+ choices.push({
801
+ name: `校准 ${result.space.issues.length} 个空间差异`,
802
+ value: 'spaceCalibration',
803
+ checked: true,
804
+ });
805
+ }
663
806
  if (choices.length === 0) {
664
807
  success('没有可修复的问题');
665
808
  return;
@@ -687,7 +830,8 @@ async function selectiveFix(issues, options) {
687
830
  orphanLibraries: selected.includes('orphanLibraries')
688
831
  ? issues.orphanLibraries
689
832
  : [],
690
- missingLibraries: [], // 缺失库无法修复
833
+ // 本地未缓存依赖是用户允许的缓存状态,修复流程不下载也不移除依赖记录。
834
+ missingLibraries: [],
691
835
  staleReferences: selected.includes('staleReferences')
692
836
  ? issues.staleReferences
693
837
  : [],
@@ -695,14 +839,76 @@ async function selectiveFix(issues, options) {
695
839
  ? issues.corruptedStores
696
840
  : [],
697
841
  };
698
- await fixAllIssues(toFix, { force: options.force });
699
- // 缺失库无法自动修复,需要提示用户
700
- if (issues.missingLibraries.length > 0) {
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}`);
701
888
  blank();
702
- hint(`缺失库 (${issues.missingLibraries.length} 个) 需要通过 td link 重新下载`);
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} 项,将忽略并保留`);
703
907
  }
908
+ blank();
704
909
  }
705
- function showDetails(issues) {
910
+ function showDetails(result) {
911
+ const issues = result.integrity;
706
912
  blank();
707
913
  title('问题详情');
708
914
  blank();
@@ -728,7 +934,7 @@ function showDetails(issues) {
728
934
  blank();
729
935
  }
730
936
  if (issues.missingLibraries.length > 0) {
731
- info(`缺失库 (${issues.missingLibraries.length}):`);
937
+ info(`本地未缓存依赖 (${issues.missingLibraries.length}):`);
732
938
  for (const lib of issues.missingLibraries) {
733
939
  hint(` - ${lib.libName}/${lib.commit.slice(0, 7)} (引用自 ${lib.project})`);
734
940
  }
@@ -755,22 +961,61 @@ function showDetails(issues) {
755
961
  }
756
962
  blank();
757
963
  }
964
+ showSpaceDetails(result.space);
758
965
  }
759
- async function fixAllIssues(issues, options) {
760
- const totalIssues = issues.invalidProjects.length +
966
+ function integrityRepairCount(issues) {
967
+ return (issues.invalidProjects.length +
761
968
  issues.danglingLinks.length +
762
969
  issues.orphanLibraries.length +
763
970
  issues.staleReferences.length +
764
- issues.corruptedStores.length;
765
- if (totalIssues === 0) {
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) {
766
1006
  success('没有需要修复的问题');
767
1007
  return;
768
1008
  }
1009
+ debug(`[check] 修复请求: integrityIssues=${totalIssues}, calibrateSpace=${options.calibrateSpace}, reportedSpaceIssues=${spaceIssueCount}, unregisteredAction=${options.deleteUnregistered ? 'delete-unreferenced' : 'keep'}, force=${options.force}, sourceSnapshot=${space.snapshotId ?? 'none'}`);
769
1010
  // 确认
770
1011
  if (!options.force) {
771
1012
  blank();
1013
+ const targets = [
1014
+ ...(totalIssues > 0 ? [`${totalIssues} 个数据问题`] : []),
1015
+ ...(spaceIssueCount > 0 ? [`${spaceIssueCount} 个空间差异`] : []),
1016
+ ];
772
1017
  const confirmed = await confirmWithCancel({
773
- message: `确认修复以上 ${totalIssues} 个问题?`,
1018
+ message: `确认处理${targets.join('和')}?`,
774
1019
  default: false,
775
1020
  });
776
1021
  if (confirmed === PROMPT_CANCELLED || !confirmed) {
@@ -782,7 +1027,100 @@ async function fixAllIssues(issues, options) {
782
1027
  separator();
783
1028
  info('正在修复...');
784
1029
  blank();
785
- await withGlobalLock(() => applyAllIssueFixes(issues));
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}`);
786
1124
  }
787
1125
  function commitKey(libName, commit) {
788
1126
  return JSON.stringify([libName, commit]);
@@ -839,6 +1177,7 @@ async function applyAllIssueFixes(issues) {
839
1177
  const recovered = await executor.recoverPending();
840
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}`);
841
1179
  let fixed = 0;
1180
+ let failed = 0;
842
1181
  // 1. 清理无效项目
843
1182
  for (const p of issues.invalidProjects) {
844
1183
  try {
@@ -848,6 +1187,7 @@ async function applyAllIssueFixes(issues) {
848
1187
  }
849
1188
  catch (err) {
850
1189
  error(`[err] 清理项目失败: ${p.path} - ${err.message}`);
1190
+ failed++;
851
1191
  }
852
1192
  }
853
1193
  // 2. 移除悬挂链接
@@ -856,7 +1196,11 @@ async function applyAllIssueFixes(issues) {
856
1196
  await fs.unlink(link.path);
857
1197
  const project = registry.getProject(link.projectHash);
858
1198
  if (project) {
859
- project.dependencies = project.dependencies.filter((d) => !(d.libName === link.dep.libName && d.commit === link.dep.commit));
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 ?? '')));
860
1204
  registry.updateProject(link.projectHash, {
861
1205
  dependencies: project.dependencies,
862
1206
  });
@@ -866,6 +1210,7 @@ async function applyAllIssueFixes(issues) {
866
1210
  }
867
1211
  catch (err) {
868
1212
  error(`[err] 移除链接失败: ${link.path} - ${err.message}`);
1213
+ failed++;
869
1214
  }
870
1215
  }
871
1216
  // 3. 删除孤立库。显式修复允许清理该版本中的未登记内容。
@@ -886,9 +1231,11 @@ async function applyAllIssueFixes(issues) {
886
1231
  for (const failure of result?.failed ?? []) {
887
1232
  error(`[err] 删除孤立库失败: ${failure.id.libName}/${failure.id.commit.slice(0, 7)} - ${failure.message}`);
888
1233
  }
1234
+ failed += result?.failed.length ?? (orphanSelections.length > 0 ? 1 : 0);
889
1235
  }
890
1236
  catch (err) {
891
1237
  error(`[err] 删除孤立库失败: ${err.message}`);
1238
+ failed++;
892
1239
  }
893
1240
  // 4. 移除失效引用
894
1241
  for (const ref of issues.staleReferences) {
@@ -898,6 +1245,7 @@ async function applyAllIssueFixes(issues) {
898
1245
  const colonIndex = ref.libKey.lastIndexOf(':');
899
1246
  if (colonIndex === -1) {
900
1247
  error(`[err] 无效的 libKey: ${ref.libKey}`);
1248
+ failed++;
901
1249
  continue;
902
1250
  }
903
1251
  const libName = ref.libKey.slice(0, colonIndex);
@@ -911,6 +1259,7 @@ async function applyAllIssueFixes(issues) {
911
1259
  }
912
1260
  catch (err) {
913
1261
  error(`[err] 移除引用失败: ${ref.libKey} - ${err.message}`);
1262
+ failed++;
914
1263
  }
915
1264
  }
916
1265
  // 5. 清理损坏的 Store 条目。共享冲突整版本处理,其他问题按平台处理。
@@ -948,16 +1297,24 @@ async function applyAllIssueFixes(issues) {
948
1297
  for (const failure of result?.failed ?? []) {
949
1298
  error(`清理损坏 Store 失败: ${failure.id.libName}/${failure.id.commit.slice(0, 7)}/${failure.platform ?? 'all'} - ${failure.message}`);
950
1299
  }
1300
+ failed += result?.failed.length ?? (cleanup.selections.length > 0 ? 1 : 0);
951
1301
  }
952
1302
  catch (err) {
953
1303
  error(`清理损坏 Store 失败: category=${cleanup.category} - ${err.message}`);
1304
+ failed++;
954
1305
  }
955
1306
  }
956
1307
  await registry.save();
957
1308
  blank();
958
1309
  separator();
959
- success(`修复完成: ${fixed} 个问题已解决`);
960
- debug(`[check] 修复完成: fixed=${fixed}`);
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 };
961
1318
  }
962
1319
  // ============ 工具函数 ============
963
1320
  async function getDirSizeRecursive(dirPath) {
@@ -1005,6 +1362,12 @@ export async function verifyIntegrity() {
1005
1362
  export async function collectCheckResultForTest(options) {
1006
1363
  return collectAllIssues(options);
1007
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
+ }
1008
1371
  /**
1009
1372
  * 修复问题(兼容旧 repair 命令)
1010
1373
  */
@@ -1012,16 +1375,22 @@ export async function repairIssues(options) {
1012
1375
  const result = await collectAllIssues();
1013
1376
  // 渲染报告
1014
1377
  renderReport(result);
1015
- const hasIssues = result.summary.integrityIssues > 0;
1378
+ const hasIssues = result.summary.integrityIssues > 0 || hasSpaceCalibrationChoice(result.space);
1016
1379
  if (!hasIssues) {
1017
1380
  return;
1018
1381
  }
1019
1382
  if (options.dryRun) {
1020
1383
  blank();
1021
- hint('运行 td check --fix 修复问题');
1384
+ hint('运行 td check --fix 修复可修复问题');
1022
1385
  return;
1023
1386
  }
1024
- await fixAllIssues(result.integrity, { force: options.force });
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);
1025
1394
  }
1026
1395
  /**
1027
1396
  * 校验所有 Store 条目的文件完整性