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.
Files changed (132) hide show
  1. package/README.md +26 -5
  2. package/dist/commands/calibrate.d.ts +10 -0
  3. package/dist/commands/calibrate.d.ts.map +1 -0
  4. package/dist/commands/calibrate.js +158 -0
  5. package/dist/commands/calibrate.js.map +1 -0
  6. package/dist/commands/check.d.ts +45 -1
  7. package/dist/commands/check.d.ts.map +1 -1
  8. package/dist/commands/check.js +322 -101
  9. package/dist/commands/check.js.map +1 -1
  10. package/dist/commands/clean.d.ts +2 -6
  11. package/dist/commands/clean.d.ts.map +1 -1
  12. package/dist/commands/clean.js +224 -209
  13. package/dist/commands/clean.js.map +1 -1
  14. package/dist/commands/dashboard.d.ts.map +1 -1
  15. package/dist/commands/dashboard.js +56 -28
  16. package/dist/commands/dashboard.js.map +1 -1
  17. package/dist/commands/link.d.ts +8 -7
  18. package/dist/commands/link.d.ts.map +1 -1
  19. package/dist/commands/link.js +717 -221
  20. package/dist/commands/link.js.map +1 -1
  21. package/dist/commands/migrate.d.ts +11 -0
  22. package/dist/commands/migrate.d.ts.map +1 -1
  23. package/dist/commands/migrate.js +277 -57
  24. package/dist/commands/migrate.js.map +1 -1
  25. package/dist/commands/reset.d.ts.map +1 -1
  26. package/dist/commands/reset.js +65 -41
  27. package/dist/commands/reset.js.map +1 -1
  28. package/dist/commands/status.d.ts +4 -0
  29. package/dist/commands/status.d.ts.map +1 -1
  30. package/dist/commands/status.js +182 -40
  31. package/dist/commands/status.js.map +1 -1
  32. package/dist/commands/unavailable.d.ts.map +1 -1
  33. package/dist/commands/unavailable.js +7 -2
  34. package/dist/commands/unavailable.js.map +1 -1
  35. package/dist/commands/unlink.d.ts.map +1 -1
  36. package/dist/commands/unlink.js +43 -39
  37. package/dist/commands/unlink.js.map +1 -1
  38. package/dist/core/codepac.d.ts +44 -0
  39. package/dist/core/codepac.d.ts.map +1 -1
  40. package/dist/core/codepac.js +287 -5
  41. package/dist/core/codepac.js.map +1 -1
  42. package/dist/core/config.d.ts +3 -1
  43. package/dist/core/config.d.ts.map +1 -1
  44. package/dist/core/config.js +7 -3
  45. package/dist/core/config.js.map +1 -1
  46. package/dist/core/linker.d.ts +7 -0
  47. package/dist/core/linker.d.ts.map +1 -1
  48. package/dist/core/linker.js +47 -0
  49. package/dist/core/linker.js.map +1 -1
  50. package/dist/core/parser.d.ts +30 -5
  51. package/dist/core/parser.d.ts.map +1 -1
  52. package/dist/core/parser.js +376 -57
  53. package/dist/core/parser.js.map +1 -1
  54. package/dist/core/registry.d.ts +11 -29
  55. package/dist/core/registry.d.ts.map +1 -1
  56. package/dist/core/registry.js +46 -113
  57. package/dist/core/registry.js.map +1 -1
  58. package/dist/core/space/calibration-executor.d.ts +16 -0
  59. package/dist/core/space/calibration-executor.d.ts.map +1 -0
  60. package/dist/core/space/calibration-executor.js +142 -0
  61. package/dist/core/space/calibration-executor.js.map +1 -0
  62. package/dist/core/space/calibration-planner.d.ts +5 -0
  63. package/dist/core/space/calibration-planner.d.ts.map +1 -0
  64. package/dist/core/space/calibration-planner.js +157 -0
  65. package/dist/core/space/calibration-planner.js.map +1 -0
  66. package/dist/core/space/cleanup-executor.d.ts +15 -0
  67. package/dist/core/space/cleanup-executor.d.ts.map +1 -0
  68. package/dist/core/space/cleanup-executor.js +188 -0
  69. package/dist/core/space/cleanup-executor.js.map +1 -0
  70. package/dist/core/space/cleanup-journal.d.ts +43 -0
  71. package/dist/core/space/cleanup-journal.d.ts.map +1 -0
  72. package/dist/core/space/cleanup-journal.js +96 -0
  73. package/dist/core/space/cleanup-journal.js.map +1 -0
  74. package/dist/core/space/cleanup-planner.d.ts +14 -0
  75. package/dist/core/space/cleanup-planner.d.ts.map +1 -0
  76. package/dist/core/space/cleanup-planner.js +166 -0
  77. package/dist/core/space/cleanup-planner.js.map +1 -0
  78. package/dist/core/space/cleanup-selection.d.ts +10 -0
  79. package/dist/core/space/cleanup-selection.d.ts.map +1 -0
  80. package/dist/core/space/cleanup-selection.js +84 -0
  81. package/dist/core/space/cleanup-selection.js.map +1 -0
  82. package/dist/core/space/estimator.d.ts +11 -0
  83. package/dist/core/space/estimator.d.ts.map +1 -0
  84. package/dist/core/space/estimator.js +142 -0
  85. package/dist/core/space/estimator.js.map +1 -0
  86. package/dist/core/space/inventory.d.ts +15 -0
  87. package/dist/core/space/inventory.d.ts.map +1 -0
  88. package/dist/core/space/inventory.js +587 -0
  89. package/dist/core/space/inventory.js.map +1 -0
  90. package/dist/core/space/meter.d.ts +5 -0
  91. package/dist/core/space/meter.d.ts.map +1 -0
  92. package/dist/core/space/meter.js +87 -0
  93. package/dist/core/space/meter.js.map +1 -0
  94. package/dist/core/space/migration-journal.d.ts +31 -0
  95. package/dist/core/space/migration-journal.d.ts.map +1 -0
  96. package/dist/core/space/migration-journal.js +121 -0
  97. package/dist/core/space/migration-journal.js.map +1 -0
  98. package/dist/core/space/service.d.ts +26 -0
  99. package/dist/core/space/service.d.ts.map +1 -0
  100. package/dist/core/space/service.js +54 -0
  101. package/dist/core/space/service.js.map +1 -0
  102. package/dist/core/space/types.d.ts +241 -0
  103. package/dist/core/space/types.d.ts.map +1 -0
  104. package/dist/core/space/types.js +13 -0
  105. package/dist/core/space/types.js.map +1 -0
  106. package/dist/core/store.d.ts +2 -0
  107. package/dist/core/store.d.ts.map +1 -1
  108. package/dist/core/store.js +44 -12
  109. package/dist/core/store.js.map +1 -1
  110. package/dist/index.js +2 -0
  111. package/dist/index.js.map +1 -1
  112. package/dist/types/index.d.ts +63 -10
  113. package/dist/types/index.d.ts.map +1 -1
  114. package/dist/types/index.js.map +1 -1
  115. package/dist/utils/atomic-file.d.ts +6 -0
  116. package/dist/utils/atomic-file.d.ts.map +1 -0
  117. package/dist/utils/atomic-file.js +37 -0
  118. package/dist/utils/atomic-file.js.map +1 -0
  119. package/dist/utils/fs-utils.d.ts +18 -1
  120. package/dist/utils/fs-utils.d.ts.map +1 -1
  121. package/dist/utils/fs-utils.js +150 -2
  122. package/dist/utils/fs-utils.js.map +1 -1
  123. package/dist/utils/git.d.ts +20 -1
  124. package/dist/utils/git.d.ts.map +1 -1
  125. package/dist/utils/git.js +181 -4
  126. package/dist/utils/git.js.map +1 -1
  127. package/dist/utils/logger.d.ts +1 -1
  128. package/dist/utils/logger.js +2 -2
  129. package/dist/utils/prompt.d.ts.map +1 -1
  130. package/dist/utils/prompt.js +4 -4
  131. package/dist/utils/prompt.js.map +1 -1
  132. package/package.json +1 -1
@@ -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 { isCodepacInstalled } from '../core/codepac.js';
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
- const result = await collectAllIssues({ integrity: options.integrity });
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 hasIssues = result.summary.integrityIssues > 0;
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 = Object.values(environment).filter((c) => !c.ok).length;
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 = integrity.orphanLibraries.reduce((sum, lib) => sum + lib.size, 0);
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
- // 1. codepac
101
- const hasCodepac = await isCodepacInstalled();
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: hasCodepac,
104
- message: hasCodepac ? '已安装' : '未安装,无法下载库',
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
- // 无 fileCount 的做回填(captureIntegrity)
364
- try {
365
- const integrity = await store.captureIntegrity(entry.libName, entry.commit, entry.platform);
366
- const storeKey = registry.getStoreKey(entry.libName, entry.commit, entry.platform);
367
- registry.updateStore(storeKey, {
368
- size: integrity.size,
369
- fileCount: integrity.fileCount,
370
- contentHash: integrity.contentHash,
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(`[ok] 清理项目: ${p.path}`);
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
- for (const lib of issues.orphanLibraries) {
683
- try {
684
- await fs.rm(lib.path, { recursive: true, force: true });
685
- // 如果删除后 libName 目录为空,也一并删除
686
- const libDir = path.dirname(lib.path);
687
- const remaining = await fs.readdir(libDir).catch(() => ['placeholder']);
688
- if (remaining.length === 0) {
689
- await fs.rm(libDir, { recursive: true, force: true }).catch(() => { });
690
- }
691
- success(`[ok] 删除孤立库: ${lib.libName}/${lib.commit.slice(0, 7)}`);
692
- fixed++;
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
- catch (err) {
695
- error(`[err] 删除孤立库失败: ${lib.libName}/${lib.commit.slice(0, 7)} - ${err.message}`);
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
- for (const item of issues.corruptedStores) {
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
- if (item.kind === 'shared_platform_conflict') {
725
- const storeKeys = registry.getLibraryStoreKeys(item.libName, item.commit);
726
- for (const storeKey of storeKeys) {
727
- registry.removeStore(storeKey);
728
- }
729
- const commitPath = path.join(storePath, item.libName, item.commit);
730
- await fs.rm(commitPath, { recursive: true, force: true });
731
- success(`[ok] 清理损坏 Store: ${item.libName}/${item.commit.slice(0, 7)} (移除整个 commit 缓存)`);
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
- else {
734
- const storeKey = registry.getStoreKey(item.libName, item.commit, item.platform);
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(`[err] 清理损坏 Store 失败: ${item.libName}/${item.commit.slice(0, 7)}/${item.platform} - ${err.message}`);
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
- // 无 fileCount 的做回填
871
- try {
872
- const integrity = await store.captureIntegrity(entry.libName, entry.commit, entry.platform);
873
- const storeKey = registry.getStoreKey(entry.libName, entry.commit, entry.platform);
874
- registry.updateStore(storeKey, {
875
- size: integrity.size,
876
- fileCount: integrity.fileCount,
877
- contentHash: integrity.contentHash,
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
- for (const item of corrupted) {
901
- try {
902
- if (item.kind === 'shared_platform_conflict') {
903
- const storeKeys = registry.getLibraryStoreKeys(item.libName, item.commit);
904
- for (const storeKey of storeKeys) {
905
- registry.removeStore(storeKey);
906
- }
907
- const commitPath = path.join(storePath, item.libName, item.commit);
908
- await fs.rm(commitPath, { recursive: true, force: true });
909
- success(`[ok] 清理损坏 Store: ${item.libName}/${item.commit.slice(0, 7)} (移除整个 commit 缓存)`);
910
- }
911
- else {
912
- const storeKey = registry.getStoreKey(item.libName, item.commit, item.platform);
913
- registry.removeStore(storeKey);
914
- const platformPath = path.join(storePath, item.libName, item.commit, item.platform);
915
- await fs.rm(platformPath, { recursive: true, force: true });
916
- success(`[ok] 清理损坏 Store: ${item.libName}/${item.commit.slice(0, 7)}/${item.platform}`);
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
- catch (err) {
920
- error(`[err] 清理损坏 Store 失败: ${item.libName}/${item.commit.slice(0, 7)}/${item.platform} - ${err.message}`);
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();