tanmi-dock 1.0.3 → 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 (114) hide show
  1. package/README.md +18 -0
  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 +9 -1
  7. package/dist/commands/check.d.ts.map +1 -1
  8. package/dist/commands/check.js +261 -94
  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 +6 -6
  18. package/dist/commands/link.d.ts.map +1 -1
  19. package/dist/commands/link.js +417 -196
  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 +19 -24
  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 +37 -20
  31. package/dist/commands/status.js.map +1 -1
  32. package/dist/commands/unavailable.js.map +1 -1
  33. package/dist/commands/unlink.d.ts.map +1 -1
  34. package/dist/commands/unlink.js +43 -39
  35. package/dist/commands/unlink.js.map +1 -1
  36. package/dist/core/config.d.ts +3 -1
  37. package/dist/core/config.d.ts.map +1 -1
  38. package/dist/core/config.js +6 -3
  39. package/dist/core/config.js.map +1 -1
  40. package/dist/core/parser.js +1 -1
  41. package/dist/core/parser.js.map +1 -1
  42. package/dist/core/registry.d.ts +11 -29
  43. package/dist/core/registry.d.ts.map +1 -1
  44. package/dist/core/registry.js +46 -113
  45. package/dist/core/registry.js.map +1 -1
  46. package/dist/core/space/calibration-executor.d.ts +16 -0
  47. package/dist/core/space/calibration-executor.d.ts.map +1 -0
  48. package/dist/core/space/calibration-executor.js +142 -0
  49. package/dist/core/space/calibration-executor.js.map +1 -0
  50. package/dist/core/space/calibration-planner.d.ts +5 -0
  51. package/dist/core/space/calibration-planner.d.ts.map +1 -0
  52. package/dist/core/space/calibration-planner.js +157 -0
  53. package/dist/core/space/calibration-planner.js.map +1 -0
  54. package/dist/core/space/cleanup-executor.d.ts +15 -0
  55. package/dist/core/space/cleanup-executor.d.ts.map +1 -0
  56. package/dist/core/space/cleanup-executor.js +188 -0
  57. package/dist/core/space/cleanup-executor.js.map +1 -0
  58. package/dist/core/space/cleanup-journal.d.ts +43 -0
  59. package/dist/core/space/cleanup-journal.d.ts.map +1 -0
  60. package/dist/core/space/cleanup-journal.js +96 -0
  61. package/dist/core/space/cleanup-journal.js.map +1 -0
  62. package/dist/core/space/cleanup-planner.d.ts +14 -0
  63. package/dist/core/space/cleanup-planner.d.ts.map +1 -0
  64. package/dist/core/space/cleanup-planner.js +166 -0
  65. package/dist/core/space/cleanup-planner.js.map +1 -0
  66. package/dist/core/space/cleanup-selection.d.ts +10 -0
  67. package/dist/core/space/cleanup-selection.d.ts.map +1 -0
  68. package/dist/core/space/cleanup-selection.js +84 -0
  69. package/dist/core/space/cleanup-selection.js.map +1 -0
  70. package/dist/core/space/estimator.d.ts +11 -0
  71. package/dist/core/space/estimator.d.ts.map +1 -0
  72. package/dist/core/space/estimator.js +142 -0
  73. package/dist/core/space/estimator.js.map +1 -0
  74. package/dist/core/space/inventory.d.ts +15 -0
  75. package/dist/core/space/inventory.d.ts.map +1 -0
  76. package/dist/core/space/inventory.js +587 -0
  77. package/dist/core/space/inventory.js.map +1 -0
  78. package/dist/core/space/meter.d.ts +5 -0
  79. package/dist/core/space/meter.d.ts.map +1 -0
  80. package/dist/core/space/meter.js +87 -0
  81. package/dist/core/space/meter.js.map +1 -0
  82. package/dist/core/space/migration-journal.d.ts +31 -0
  83. package/dist/core/space/migration-journal.d.ts.map +1 -0
  84. package/dist/core/space/migration-journal.js +121 -0
  85. package/dist/core/space/migration-journal.js.map +1 -0
  86. package/dist/core/space/service.d.ts +26 -0
  87. package/dist/core/space/service.d.ts.map +1 -0
  88. package/dist/core/space/service.js +54 -0
  89. package/dist/core/space/service.js.map +1 -0
  90. package/dist/core/space/types.d.ts +241 -0
  91. package/dist/core/space/types.d.ts.map +1 -0
  92. package/dist/core/space/types.js +13 -0
  93. package/dist/core/space/types.js.map +1 -0
  94. package/dist/core/store.d.ts.map +1 -1
  95. package/dist/core/store.js +12 -6
  96. package/dist/core/store.js.map +1 -1
  97. package/dist/index.js +2 -0
  98. package/dist/index.js.map +1 -1
  99. package/dist/types/index.d.ts +2 -0
  100. package/dist/types/index.d.ts.map +1 -1
  101. package/dist/utils/atomic-file.d.ts +6 -0
  102. package/dist/utils/atomic-file.d.ts.map +1 -0
  103. package/dist/utils/atomic-file.js +37 -0
  104. package/dist/utils/atomic-file.js.map +1 -0
  105. package/dist/utils/fs-utils.d.ts +18 -1
  106. package/dist/utils/fs-utils.d.ts.map +1 -1
  107. package/dist/utils/fs-utils.js +150 -2
  108. package/dist/utils/fs-utils.js.map +1 -1
  109. package/dist/utils/logger.d.ts +1 -1
  110. package/dist/utils/logger.js +2 -2
  111. package/dist/utils/prompt.d.ts.map +1 -1
  112. package/dist/utils/prompt.js +4 -4
  113. package/dist/utils/prompt.js.map +1 -1
  114. package/package.json +1 -1
@@ -21,6 +21,7 @@ import { ProgressTracker, DownloadMonitor, MultiBarManager } from '../utils/prog
21
21
  import { success, warn, error, info, hint, blank, separator, debug } from '../utils/logger.js';
22
22
  import { verifyLocalCommit, findSubmoduleConfigs } from '../utils/git.js';
23
23
  import { DependencyStatus } from '../types/index.js';
24
+ import { SpaceService, buildHalfCleanupSelectionSummary, buildUnreferencedSelectionSummary, estimateDownloadSpace, } from '../core/space/service.js';
24
25
  import { withGlobalLock } from '../utils/global-lock.js';
25
26
  import { selectPlatforms, parsePlatformArgs, selectOption, selectOptionalConfigs, PROMPT_CANCELLED } from '../utils/prompt.js';
26
27
  import pLimit from 'p-limit';
@@ -75,6 +76,100 @@ function resolveRequestedPlatformsByActual(requestedPlatforms, sparse, actualPla
75
76
  missingRequested: requestedPlatforms.filter((platform) => !satisfiedRequested.includes(platform)),
76
77
  };
77
78
  }
79
+ async function verifyGeneralStoreState(dependency) {
80
+ const result = await verifyExistingPlatforms(dependency.libName, dependency.commit, [GENERAL_PLATFORM]);
81
+ return {
82
+ valid: result.valid.length > 0,
83
+ repairRequired: result.repairRequired.length > 0,
84
+ corrupted: result.corrupted.length > 0,
85
+ };
86
+ }
87
+ async function executeLinkStoreRepair(dependency, platforms, options) {
88
+ const registry = getRegistry();
89
+ const storePath = await store.getStorePath();
90
+ const spaceService = new SpaceService(registry, storePath);
91
+ const plan = await spaceService.buildCleanupPlan({
92
+ reason: 'repair',
93
+ selections: [{
94
+ id: { libName: dependency.libName, commit: dependency.commit },
95
+ platforms,
96
+ includeShared: options.includeShared,
97
+ }],
98
+ requireUnreferenced: false,
99
+ allowUnknownContent: false,
100
+ });
101
+ for (const warningMessage of plan.warnings)
102
+ warn(`[space] ${warningMessage}`);
103
+ debug(`[link] 调用统一清理执行器: category=${options.category}, plan=${plan.id}, lib=${dependency.libName}, commit=${dependency.commit}, platforms=${platforms === 'all' ? 'all' : platforms.join(',')}, includeShared=${Boolean(options.includeShared)}, groups=${plan.groups.length}, expected=${plan.expectedRelease.bytes}`);
104
+ if (plan.groups.length === 0)
105
+ return;
106
+ const result = await spaceService.createCleanupExecutor().execute(plan);
107
+ debug(`[link] 清理执行返回: category=${options.category}, plan=${plan.id}, status=${result.status}, succeeded=${result.succeeded.length}, failed=${result.failed.length}, released=${result.released.bytes}`);
108
+ if (result.status !== 'completed') {
109
+ throw new Error(`${dependency.libName}@${dependency.commit.slice(0, 8)} 缓存修复未完整完成: ${result.failed.map((item) => item.message).join('; ')}`);
110
+ }
111
+ }
112
+ async function prepareGeneralStoreRepair(dependency, repairRequiredPlatforms) {
113
+ const generalState = await verifyGeneralStoreState(dependency);
114
+ if (!generalState.valid && (generalState.corrupted || generalState.repairRequired)) {
115
+ await executeLinkStoreRepair(dependency, 'all', { category: 'general-corruption' });
116
+ if (!repairRequiredPlatforms.includes(GENERAL_PLATFORM)) {
117
+ repairRequiredPlatforms.push(GENERAL_PLATFORM);
118
+ }
119
+ return true;
120
+ }
121
+ return false;
122
+ }
123
+ async function prepareSharedStoreRepair(dependency, requestedPlatforms, repairRequiredPlatforms) {
124
+ const storePath = await config.getStorePath();
125
+ if (!storePath)
126
+ return [];
127
+ const sharedPath = path.join(storePath, dependency.libName, dependency.commit, '_shared');
128
+ try {
129
+ await fs.access(sharedPath);
130
+ }
131
+ catch {
132
+ return [];
133
+ }
134
+ const result = await verifyExistingPlatforms(dependency.libName, dependency.commit, [SHARED_PLATFORM]);
135
+ if (result.valid.length > 0 && result.corrupted.length === 0 && result.repairRequired.length === 0) {
136
+ return [];
137
+ }
138
+ const removedPlatforms = [];
139
+ for (const platform of requestedPlatforms) {
140
+ if (!repairRequiredPlatforms.includes(platform)) {
141
+ repairRequiredPlatforms.push(platform);
142
+ }
143
+ removedPlatforms.push(platform);
144
+ }
145
+ await executeLinkStoreRepair(dependency, 'all', {
146
+ includeShared: true,
147
+ category: 'shared-corruption',
148
+ });
149
+ debug(`${dependency.libName} _shared 本地缓存不完整,准备随平台 [${requestedPlatforms.join(', ') || '无'}] 重新下载`);
150
+ return removedPlatforms;
151
+ }
152
+ async function hasRequestedPlatformContent(dependency, requestedPlatforms, vars) {
153
+ if (requestedPlatforms.length === 0)
154
+ return false;
155
+ const requestedTargets = [...new Set(requestedPlatforms.flatMap((platform) => getRequestedPlatformTargets(platform, dependency.sparse, vars)))];
156
+ const storePath = await store.getStorePath();
157
+ const commitPath = path.join(storePath, dependency.libName, dependency.commit);
158
+ try {
159
+ const entries = await fs.readdir(commitPath, { withFileTypes: true });
160
+ for (const entry of entries) {
161
+ if (!entry.isDirectory() || !requestedTargets.includes(entry.name))
162
+ continue;
163
+ if (await store.exists(dependency.libName, dependency.commit, entry.name)) {
164
+ return true;
165
+ }
166
+ }
167
+ }
168
+ catch {
169
+ // Store 目录不存在时继续使用历史记录或后续 General 检测
170
+ }
171
+ return false;
172
+ }
78
173
  export function assessDownloadResult(requestedPlatforms, sparse, downloadResult, vars) {
79
174
  const downloadedActual = new Set(downloadResult.platformDirs);
80
175
  const { satisfiedRequested, missingRequested, } = resolveRequestedPlatformsByActual(requestedPlatforms, sparse, downloadResult.platformDirs, vars);
@@ -92,8 +187,9 @@ export function assessDownloadResult(requestedPlatforms, sparse, downloadResult,
92
187
  isPureGeneral,
93
188
  };
94
189
  }
95
- async function resolveRequestedStoreState(dependency, requestedPlatforms, vars) {
190
+ async function resolveRequestedStoreState(dependency, requestedPlatforms, vars, repairMissingRecords = true) {
96
191
  const resolvedExisting = [];
192
+ const stalePlatforms = [];
97
193
  const candidatePlatforms = [...new Set(requestedPlatforms.flatMap((platform) => getRequestedPlatformTargets(platform, dependency.sparse, vars)))];
98
194
  for (const platform of candidatePlatforms) {
99
195
  if (await store.exists(dependency.libName, dependency.commit, platform)) {
@@ -103,13 +199,13 @@ async function resolveRequestedStoreState(dependency, requestedPlatforms, vars)
103
199
  const registry = getRegistry();
104
200
  const storeKey = registry.getStoreKey(dependency.libName, dependency.commit, platform);
105
201
  if (registry.getStore(storeKey)) {
106
- registry.removeStore(storeKey);
107
- const storePath = await store.getStorePath();
108
- const platformPath = store.getLibraryPath(storePath, dependency.libName, dependency.commit, platform);
109
- await fs.rm(platformPath, { recursive: true, force: true }).catch(() => { });
202
+ stalePlatforms.push(platform);
110
203
  }
111
204
  }
112
205
  }
206
+ if (repairMissingRecords && stalePlatforms.length > 0) {
207
+ await executeLinkStoreRepair(dependency, stalePlatforms, { category: 'missing-platform-path' });
208
+ }
113
209
  const result = resolveRequestedPlatformsByActual(requestedPlatforms, dependency.sparse, resolvedExisting, vars);
114
210
  debug(`[link-store-state] ${dependency.libName}@${dependency.commit.slice(0, 7)} 请求平台=${requestedPlatforms.join(', ') || '空'}, Store候选=${candidatePlatforms.join(', ') || '空'}, 实际存在=${result.actualExisting.join(', ') || '无'}, 满足请求=${result.satisfiedRequested.join(', ') || '无'}, 缺失请求=${result.missingRequested.join(', ') || '无'}`);
115
211
  return result;
@@ -170,7 +266,7 @@ function upsertLibraryAvailability(dependency, updates) {
170
266
  lastAccess: new Date().toISOString(),
171
267
  });
172
268
  }
173
- export async function resolveLibraryGeneralState(dependency, requestedPlatforms = [], vars) {
269
+ export async function resolveLibraryGeneralState(dependency, requestedPlatforms = [], vars, updateRegistry = true) {
174
270
  const registry = getRegistry();
175
271
  const libKey = registry.getLibraryKey(dependency.libName, dependency.commit);
176
272
  const library = registry.getLibrary(libKey);
@@ -182,9 +278,9 @@ export async function resolveLibraryGeneralState(dependency, requestedPlatforms
182
278
  && library?.isGeneral === false
183
279
  && library.platforms.some((platform) => platform !== GENERAL_PLATFORM);
184
280
  if (requestedPlatforms.length > 0) {
185
- const requestedState = await resolveRequestedStoreState(dependency, requestedPlatforms, vars);
281
+ const requestedState = await resolveRequestedStoreState(dependency, requestedPlatforms, vars, updateRegistry);
186
282
  if (requestedState.actualExisting.length > 0) {
187
- if (library?.isGeneral === true) {
283
+ if (updateRegistry && library?.isGeneral === true) {
188
284
  registry.updateLibrary(libKey, {
189
285
  platforms: requestedState.actualExisting,
190
286
  isGeneral: false,
@@ -195,30 +291,12 @@ export async function resolveLibraryGeneralState(dependency, requestedPlatforms
195
291
  }
196
292
  }
197
293
  if (requestedPlatforms.length > 0 && library?.isGeneral === true) {
198
- const requestedTargets = [...new Set(requestedPlatforms.flatMap((platform) => getRequestedPlatformTargets(platform, dependency.sparse, vars)))];
199
- const storePath = await store.getStorePath();
200
- const commitPath = path.join(storePath, dependency.libName, dependency.commit);
201
- try {
202
- const entries = await fs.readdir(commitPath, { withFileTypes: true });
203
- let hasRequestedPlatformDir = false;
204
- for (const entry of entries) {
205
- if (!entry.isDirectory() || !requestedTargets.includes(entry.name))
206
- continue;
207
- if (await store.exists(dependency.libName, dependency.commit, entry.name)) {
208
- hasRequestedPlatformDir = true;
209
- break;
210
- }
211
- }
212
- if (hasRequestedPlatformDir) {
213
- return false;
214
- }
215
- }
216
- catch {
217
- // Store 目录不存在时继续使用历史记录或后续 General 检测
294
+ if (await hasRequestedPlatformContent(dependency, requestedPlatforms, vars)) {
295
+ return false;
218
296
  }
219
297
  }
220
298
  if (requestedPlatforms.length > 0 && hasPlatformSparse) {
221
- if (library?.isGeneral === true) {
299
+ if (updateRegistry && library?.isGeneral === true) {
222
300
  registry.updateLibrary(libKey, {
223
301
  isGeneral: false,
224
302
  lastAccess: new Date().toISOString(),
@@ -231,16 +309,21 @@ export async function resolveLibraryGeneralState(dependency, requestedPlatforms
231
309
  }
232
310
  if (typeof library?.isGeneral === 'boolean') {
233
311
  if (library.isGeneral) {
234
- const hasGeneralContent = await store.exists(dependency.libName, dependency.commit, GENERAL_PLATFORM);
235
- if (!hasGeneralContent) {
236
- registry.updateLibrary(libKey, {
237
- isGeneral: false,
238
- lastAccess: new Date().toISOString(),
239
- });
312
+ const generalState = await verifyGeneralStoreState(dependency);
313
+ if (!generalState.valid) {
314
+ if (updateRegistry && (generalState.corrupted || generalState.repairRequired)) {
315
+ await executeLinkStoreRepair(dependency, 'all', { category: 'general-state-repair' });
316
+ }
317
+ if (updateRegistry) {
318
+ registry.updateLibrary(libKey, {
319
+ isGeneral: false,
320
+ lastAccess: new Date().toISOString(),
321
+ });
322
+ }
240
323
  return false;
241
324
  }
242
325
  const actualGeneral = await store.isGeneralLib(dependency.libName, dependency.commit);
243
- if (!actualGeneral) {
326
+ if (updateRegistry && !actualGeneral) {
244
327
  registry.updateLibrary(libKey, {
245
328
  isGeneral: false,
246
329
  lastAccess: new Date().toISOString(),
@@ -249,7 +332,7 @@ export async function resolveLibraryGeneralState(dependency, requestedPlatforms
249
332
  return actualGeneral;
250
333
  }
251
334
  const actualGeneral = await store.isGeneralLib(dependency.libName, dependency.commit);
252
- if (actualGeneral) {
335
+ if (updateRegistry && actualGeneral) {
253
336
  registry.updateLibrary(libKey, {
254
337
  platforms: [GENERAL_PLATFORM],
255
338
  isGeneral: true,
@@ -463,7 +546,7 @@ async function linkScope(params) {
463
546
  blank();
464
547
  // 2. 分类依赖
465
548
  const scopePath = path.dirname(path.dirname(configPath));
466
- const classified = await classifyDependencies(dependencies, scopePath, configPath, platforms, configVars);
549
+ const classified = await classifyDependencies(dependencies, scopePath, configPath, platforms, configVars, !dryRun);
467
550
  const stats = {
468
551
  linked: 0, relink: 0, replace: 0, absorb: 0, missing: 0, linkNew: 0,
469
552
  };
@@ -480,14 +563,31 @@ async function linkScope(params) {
480
563
  }
481
564
  // 4. 磁盘空间预检
482
565
  if (stats.missing > 0 && download) {
483
- const estimatedSize = stats.missing * 500 * 1024 * 1024;
484
- const spaceCheck = await checkDiskSpace(storePath, estimatedSize);
485
- if (!spaceCheck.sufficient) {
486
- error(`磁盘空间不足: 预计需要 ${formatSize(spaceCheck.required)},可用 ${formatSize(spaceCheck.available)}(含 1GB 安全余量)`);
487
- process.exit(EXIT_CODES.IOERR);
566
+ const downloadRequests = classified
567
+ .filter((item) => item.status === DependencyStatus.MISSING)
568
+ .map((item) => ({
569
+ libName: item.dependency.libName,
570
+ commit: item.dependency.commit,
571
+ platforms: platforms.filter((platform) => !getBlockedRequestedPlatforms(registry, item.dependency, platforms, configVars).includes(platform)),
572
+ }))
573
+ .filter((request) => request.platforms.length > 0);
574
+ const estimate = estimateDownloadSpace(registry, downloadRequests);
575
+ for (const evidence of estimate.evidence)
576
+ debug(`[space] 下载容量依据: ${evidence}`);
577
+ if (estimate.status === 'unknown' || !estimate.amount) {
578
+ warn(`下载容量未知: ${estimate.warnings.join('; ')}`);
579
+ debug(`[space] 下载容量预检跳过: requests=${downloadRequests.length}, status=${estimate.status}, warnings=${estimate.warnings.length}`);
488
580
  }
489
- if (spaceCheck.available === 0) {
490
- warn('无法获取磁盘空间信息,继续执行');
581
+ else if (downloadRequests.length > 0) {
582
+ const spaceCheck = await checkDiskSpace(storePath, estimate.amount.bytes);
583
+ debug(`[space] 下载容量预检: requests=${downloadRequests.length}, estimated=${estimate.amount.bytes}, available=${spaceCheck.available}, required=${spaceCheck.required}, sufficient=${spaceCheck.sufficient}`);
584
+ if (!spaceCheck.sufficient) {
585
+ error(`磁盘空间不足: 预计需要 ${formatSize(spaceCheck.required)},可用 ${formatSize(spaceCheck.available)}(含 1GB 安全余量)`);
586
+ process.exit(EXIT_CODES.IOERR);
587
+ }
588
+ if (spaceCheck.available === 0) {
589
+ warn('无法获取磁盘空间信息,继续执行');
590
+ }
491
591
  }
492
592
  }
493
593
  // 5. 预扫描额外平台(仅主项目 scope)
@@ -601,7 +701,7 @@ async function linkScope(params) {
601
701
  }
602
702
  // 7. 逐个处理依赖(LINKED/RELINK/REPLACE/ABSORB/LINK_NEW)
603
703
  for (const item of classified) {
604
- const { dependency, status, localPath } = item;
704
+ const { dependency, status, localPath, repairRequiredPlatforms } = item;
605
705
  await store.ensureCompatibleStore(storePath, dependency.libName, dependency.commit);
606
706
  switch (status) {
607
707
  case DependencyStatus.LINKED: {
@@ -612,6 +712,7 @@ async function linkScope(params) {
612
712
  const supplementResult = await supplementMissingPlatforms(dependency, platforms, registry, tx, {
613
713
  vars: configVars,
614
714
  useGitLightweightDownload: gitLightweightDownload,
715
+ repairRequiredPlatforms,
615
716
  });
616
717
  await registerNestedLibraries(supplementResult.nestedLibraries, projectHash);
617
718
  if (supplementResult.downloaded.length > 0) {
@@ -648,6 +749,7 @@ async function linkScope(params) {
648
749
  const relinkSupplementResult = await supplementMissingPlatforms(dependency, platforms, registry, tx, {
649
750
  vars: configVars,
650
751
  useGitLightweightDownload: gitLightweightDownload,
752
+ repairRequiredPlatforms,
651
753
  });
652
754
  await registerNestedLibraries(relinkSupplementResult.nestedLibraries, projectHash);
653
755
  const { actualExisting: relinkExisting, satisfiedRequested, } = await resolveRequestedStoreState(dependency, requestedPlatforms, configVars);
@@ -705,6 +807,7 @@ async function linkScope(params) {
705
807
  const replaceSupplementResult = await supplementMissingPlatforms(dependency, platforms, registry, tx, {
706
808
  vars: configVars,
707
809
  useGitLightweightDownload: gitLightweightDownload,
810
+ repairRequiredPlatforms,
708
811
  });
709
812
  await registerNestedLibraries(replaceSupplementResult.nestedLibraries, projectHash);
710
813
  const { actualExisting: replaceExisting, satisfiedRequested, } = await resolveRequestedStoreState(dependency, requestedPlatforms, configVars);
@@ -814,6 +917,7 @@ async function linkScope(params) {
814
917
  const absorbSupplementResult = await supplementMissingPlatforms(dependency, platforms, registry, tx, {
815
918
  vars: configVars,
816
919
  useGitLightweightDownload: gitLightweightDownload,
920
+ repairRequiredPlatforms,
817
921
  });
818
922
  await registerNestedLibraries(absorbSupplementResult.nestedLibraries, projectHash);
819
923
  if (absorbSupplementResult.downloaded.length > 0) {
@@ -880,9 +984,20 @@ async function linkScope(params) {
880
984
  const blockedPlatforms = getBlockedRequestedPlatforms(registry, dependency, platforms, configVars);
881
985
  const requestedPlatforms = platforms.filter((platform) => !blockedPlatforms.includes(platform));
882
986
  const { missingRequested: missing } = await resolveRequestedStoreState(dependency, requestedPlatforms, configVars);
987
+ const repairRequiredSet = new Set(repairRequiredPlatforms ?? []);
988
+ const repairDownloadTargets = missing.filter((platform) => repairRequiredSet.has(platform));
989
+ const isRepairingGeneral = repairRequiredSet.has(GENERAL_PLATFORM);
883
990
  const linkNewLibId = `${dependency.libName}@${dependency.commit}`;
884
991
  if (missing.length > 0 && !skipAllDownloads && downloadConfirmedLibs.has(linkNewLibId)) {
885
- info(`${dependency.libName} 缺少平台 [${missing.join(', ')}],开始下载...`);
992
+ if (repairDownloadTargets.length > 0 || isRepairingGeneral) {
993
+ const repairTargetText = repairDownloadTargets.length > 0
994
+ ? repairDownloadTargets.join(', ')
995
+ : GENERAL_PLATFORM;
996
+ debug(`${dependency.libName} ${repairTargetText} 本地缓存不完整,开始重新下载`);
997
+ }
998
+ else {
999
+ info(`${dependency.libName} 缺少平台 [${missing.join(', ')}],开始下载...`);
1000
+ }
886
1001
  const linkNewLibKey = registry.getLibraryKey(dependency.libName, dependency.commit);
887
1002
  const linkNewHistoryLib = registry.getLibrary(linkNewLibKey);
888
1003
  const linkNewMonitor = new DownloadMonitor({
@@ -898,6 +1013,17 @@ async function linkScope(params) {
898
1013
  onHeartbeat: (message) => { linkNewMonitor.heartbeat(message); },
899
1014
  });
900
1015
  }
1016
+ catch (err) {
1017
+ if (repairDownloadTargets.length > 0 || isRepairingGeneral) {
1018
+ const repairTargetText = repairDownloadTargets.length > 0
1019
+ ? repairDownloadTargets.join(', ')
1020
+ : GENERAL_PLATFORM;
1021
+ error(`${dependency.libName} ${repairTargetText} 本地缓存不完整,自动重新下载失败`);
1022
+ hint(`可运行 td reset ${dependency.libName} 后重试`);
1023
+ debug(`${dependency.libName} 下载失败: ${err.message}`);
1024
+ }
1025
+ throw err;
1026
+ }
901
1027
  finally {
902
1028
  await linkNewMonitor.stop();
903
1029
  }
@@ -912,6 +1038,10 @@ async function linkScope(params) {
912
1038
  tx.recordOp('absorb', linkNewCommitPath, downloadResult.libDir);
913
1039
  const linkNewAbsorbResult = await store.absorbLib(downloadResult.libDir, filteredDownloaded, dependency.libName, dependency.commit);
914
1040
  await registerNestedLibraries(linkNewAbsorbResult.nestedLibraries, projectHash);
1041
+ const repaired = filteredDownloaded.filter((platform) => repairRequiredSet.has(platform));
1042
+ if (repaired.length > 0) {
1043
+ warn(`${dependency.libName} ${repaired.join(', ')} 本地缓存不完整,已重新下载`);
1044
+ }
915
1045
  }
916
1046
  }
917
1047
  finally {
@@ -927,7 +1057,12 @@ async function linkScope(params) {
927
1057
  await linker.linkGeneral(localPath, sharedPath);
928
1058
  await ensureLinkedRegistryState(dependency.libName, dependency.commit, dependency.branch, dependency.url, [GENERAL_PLATFORM], projectHash, true);
929
1059
  generalLibs.add(dependency.libName);
930
- success(`${dependency.libName} (${dependency.commit.slice(0, 7)}) - General 库,创建链接`);
1060
+ if (isRepairingGeneral) {
1061
+ warn(`${dependency.libName} ${GENERAL_PLATFORM} 本地缓存不完整,已重新下载`);
1062
+ }
1063
+ else {
1064
+ success(`${dependency.libName} (${dependency.commit.slice(0, 7)}) - General 库,创建链接`);
1065
+ }
931
1066
  }
932
1067
  else if (linkNewExisting.length === 0) {
933
1068
  const { KNOWN_PLATFORM_VALUES } = await import('../core/platform.js');
@@ -977,13 +1112,14 @@ async function linkScope(params) {
977
1112
  const multiBarManager = isTTY ? new MultiBarManager() : null;
978
1113
  const downloadLimit = pLimit(concurrency);
979
1114
  const downloadTasks = toDownload.map((item) => downloadLimit(async () => {
980
- const { dependency, localPath } = item;
1115
+ const { dependency, localPath, repairRequiredPlatforms = [] } = item;
1116
+ const repairRequiredSet = new Set(repairRequiredPlatforms);
981
1117
  const pLog = {
982
1118
  info: (msg) => multiBarManager ? multiBarManager.log(`[info] ${msg}`) : info(msg),
983
1119
  success: (msg) => multiBarManager ? multiBarManager.log(`[ok] ${msg}`) : success(msg),
984
1120
  hint: (msg) => multiBarManager ? multiBarManager.log(`[hint] ${msg}`) : hint(msg),
985
1121
  warn: (msg) => multiBarManager ? multiBarManager.log(`[warn] ${msg}`) : warn(msg),
986
- error: (msg) => multiBarManager ? multiBarManager.log(`[error] ${msg}`) : error(msg),
1122
+ error: (msg) => multiBarManager ? multiBarManager.log(`[err] ${msg}`) : error(msg),
987
1123
  };
988
1124
  try {
989
1125
  const storeCommitPath = path.join(storePath, dependency.libName, dependency.commit);
@@ -1038,7 +1174,17 @@ async function linkScope(params) {
1038
1174
  }
1039
1175
  return { success: false, name: dependency.libName, skipped: true, skippedPlatforms: missing, unsupported: true };
1040
1176
  }
1041
- pLog.info(`下载 ${dependency.libName} [${dlToDownload.join(', ')}]...`);
1177
+ const repairDownloadTargets = dlToDownload.filter((platform) => repairRequiredSet.has(platform));
1178
+ const isRepairingGeneral = repairRequiredSet.has(GENERAL_PLATFORM);
1179
+ if (repairDownloadTargets.length === 0 && !isRepairingGeneral) {
1180
+ pLog.info(`下载 ${dependency.libName} [${dlToDownload.join(', ')}]...`);
1181
+ }
1182
+ else {
1183
+ const repairTargetText = repairDownloadTargets.length > 0
1184
+ ? repairDownloadTargets.join(', ')
1185
+ : GENERAL_PLATFORM;
1186
+ debug(`${dependency.libName} ${repairTargetText} 本地缓存不完整,开始重新下载`);
1187
+ }
1042
1188
  const estimatedSize = historyLib?.size;
1043
1189
  const downloadMonitor = new DownloadMonitor({
1044
1190
  name: ` ${dependency.libName}`, estimatedSize, getDirSize,
@@ -1076,7 +1222,15 @@ async function linkScope(params) {
1076
1222
  await linker.linkGeneral(localPath, sharedPath);
1077
1223
  await ensureLinkedRegistryState(dependency.libName, dependency.commit, dependency.branch, dependency.url, [GENERAL_PLATFORM], projectHash, true);
1078
1224
  generalLibs.add(dependency.libName);
1079
- pLog.success(`${dependency.libName} (${dependency.commit.slice(0, 7)}) - General 库,下载完成`);
1225
+ if (isRepairingGeneral) {
1226
+ pLog.warn(`${dependency.libName} ${GENERAL_PLATFORM} 本地缓存不完整,已重新下载`);
1227
+ }
1228
+ else if (repairDownloadTargets.length > 0) {
1229
+ pLog.warn(`${dependency.libName} ${repairDownloadTargets.join(', ')} 本地缓存不完整,已重新下载`);
1230
+ }
1231
+ else {
1232
+ pLog.success(`${dependency.libName} (${dependency.commit.slice(0, 7)}) - General 库,下载完成`);
1233
+ }
1080
1234
  return { success: true, name: dependency.libName, downloadedPlatforms: [GENERAL_PLATFORM], skippedPlatforms: [], isGeneral: true };
1081
1235
  }
1082
1236
  const filteredDownloaded = assessment.downloadedRequested;
@@ -1111,7 +1265,13 @@ async function linkScope(params) {
1111
1265
  await linker.linkLib(localPath, storeCommitPath, linkPlatforms);
1112
1266
  await ensureLinkedRegistryState(dependency.libName, dependency.commit, dependency.branch, dependency.url, linkPlatforms, projectHash, false);
1113
1267
  const notLinkedPlatforms = resolveRequestedPlatformsByActual(requestedPlatforms, dependency.sparse, linkPlatforms, configVars).missingRequested;
1114
- pLog.success(`${dependency.libName} (${dependency.commit.slice(0, 7)}) - 下载完成 [${linkPlatforms.join(', ')}]`);
1268
+ const repaired = filteredDownloaded.filter((platform) => repairRequiredSet.has(platform));
1269
+ if (repaired.length > 0) {
1270
+ pLog.warn(`${dependency.libName} ${repaired.join(', ')} 本地缓存不完整,已重新下载`);
1271
+ }
1272
+ else {
1273
+ pLog.success(`${dependency.libName} (${dependency.commit.slice(0, 7)}) - 下载完成 [${linkPlatforms.join(', ')}]`);
1274
+ }
1115
1275
  return { success: true, name: dependency.libName, downloadedPlatforms: linkPlatforms, skippedPlatforms: notLinkedPlatforms };
1116
1276
  }
1117
1277
  finally {
@@ -1119,8 +1279,22 @@ async function linkScope(params) {
1119
1279
  }
1120
1280
  }
1121
1281
  catch (err) {
1122
- pLog.error(`${dependency.libName} 下载失败: ${err.message}`);
1123
- return { success: false, name: dependency.libName, error: err.message };
1282
+ const repairTargets = repairRequiredPlatforms.filter((platform) => platforms.includes(platform) || platform === GENERAL_PLATFORM);
1283
+ const repairFailure = repairTargets.length > 0;
1284
+ if (repairTargets.length > 0) {
1285
+ pLog.error(`${dependency.libName} ${repairTargets.join(', ')} 本地缓存不完整,自动重新下载失败`);
1286
+ pLog.hint(`可运行 td reset ${dependency.libName} 后重试`);
1287
+ debug(`${dependency.libName} 下载失败: ${err.message}`);
1288
+ }
1289
+ else {
1290
+ pLog.error(`${dependency.libName} 下载失败: ${err.message}`);
1291
+ }
1292
+ return {
1293
+ success: false,
1294
+ name: dependency.libName,
1295
+ error: err.message,
1296
+ repairFailure,
1297
+ };
1124
1298
  }
1125
1299
  }));
1126
1300
  const results = await Promise.all(downloadTasks);
@@ -1131,6 +1305,12 @@ async function linkScope(params) {
1131
1305
  info(`下载完成: ${succeeded.length}/${toDownload.length} 个库`);
1132
1306
  if (failed.length > 0)
1133
1307
  warn(`${failed.length} 个库下载失败`);
1308
+ const failedRepairs = failed.filter((result) => 'repairFailure' in result && result.repairFailure);
1309
+ if (failedRepairs.length > 0) {
1310
+ const names = failedRepairs.map((result) => result.name).join(', ');
1311
+ debug(`[link] 缓存修复下载失败,终止链接: libraries=${names}`);
1312
+ throw new Error(`缓存修复下载失败: ${names}`);
1313
+ }
1134
1314
  const allSkipped = [];
1135
1315
  for (const r of results) {
1136
1316
  if ('skippedPlatforms' in r && r.skippedPlatforms && r.skippedPlatforms.length > 0) {
@@ -1207,7 +1387,10 @@ async function linkScope(params) {
1207
1387
  export async function linkProject(projectPath, options) {
1208
1388
  const { absolutePath, normalizedPath, configPath: discoveredConfigPath, } = await resolveProjectRootPath(projectPath);
1209
1389
  // === 阶段 1: 初始化 ===
1210
- const cfg = await config.load();
1390
+ const cfg = await config.load({ migrate: !options.dryRun });
1391
+ if (options.dryRun && cfg && config.checkConfigVersion(cfg) === 'migrate') {
1392
+ throw new Error('配置需要迁移;dry-run 不会修改配置,请先运行一次普通命令完成迁移');
1393
+ }
1211
1394
  if (cfg?.logLevel)
1212
1395
  setLogLevel(cfg.logLevel);
1213
1396
  if (cfg?.proxy)
@@ -1215,7 +1398,7 @@ export async function linkProject(projectPath, options) {
1215
1398
  const concurrency = cfg?.concurrency ?? 5;
1216
1399
  const gitLightweightDownload = cfg?.gitLightweightDownload ?? true;
1217
1400
  const registry = getRegistry();
1218
- await registry.load();
1401
+ await registry.load({ migrate: !options.dryRun });
1219
1402
  const existingProject = registry.getProjectByPath(normalizedPath);
1220
1403
  const rememberedPlatforms = existingProject?.platforms;
1221
1404
  // 确定平台列表
@@ -1348,24 +1531,37 @@ export async function linkProject(projectPath, options) {
1348
1531
  }
1349
1532
  }
1350
1533
  const finalPath = normalizedRoot;
1351
- // 检查是否有未完成的事务需要恢复
1352
- const pendingTx = await Transaction.findPending();
1353
- if (pendingTx) {
1354
- warn(`发现未完成的事务 (${pendingTx.id.slice(0, 8)})`);
1355
- info('正在尝试回滚...');
1356
- try {
1357
- await pendingTx.rollback();
1358
- success('事务回滚完成');
1359
- }
1360
- catch (err) {
1361
- error(`回滚失败: ${err.message}`);
1362
- hint('请手动检查 Store 和项目目录状态');
1534
+ // 预演模式只读取现状,不恢复历史事务,也不创建新的持久事务日志。
1535
+ if (!options.dryRun) {
1536
+ const pendingTx = await Transaction.findPending();
1537
+ if (pendingTx) {
1538
+ warn(`发现未完成的事务 (${pendingTx.id.slice(0, 8)})`);
1539
+ info('正在尝试回滚...');
1540
+ try {
1541
+ await pendingTx.rollback();
1542
+ success('事务回滚完成');
1543
+ }
1544
+ catch (err) {
1545
+ error(`回滚失败: ${err.message}`);
1546
+ hint('请手动检查 Store 和项目目录状态');
1547
+ }
1363
1548
  }
1364
1549
  }
1550
+ else {
1551
+ const pendingTransactions = await Transaction.getPendingTransactions();
1552
+ debug(`[link] dry-run 跳过事务恢复与事务日志创建: pending=${pendingTransactions.length}`);
1553
+ }
1365
1554
  const storePath = await store.getStorePath();
1555
+ if (!options.dryRun) {
1556
+ const recovered = await new SpaceService(registry, storePath)
1557
+ .createCleanupExecutor()
1558
+ .recoverPending();
1559
+ debug(`[link] 清理日志恢复完成: recovered=${recovered}, store=${storePath}`);
1560
+ }
1366
1561
  const projectHash = registry.hashPath(finalPath);
1367
1562
  const tx = new Transaction(`link:${finalPath}`);
1368
- await tx.begin();
1563
+ if (!options.dryRun)
1564
+ await tx.begin();
1369
1565
  // === 阶段 4: 依次执行 linkScope ===
1370
1566
  try {
1371
1567
  // 主项目
@@ -1514,52 +1710,44 @@ export async function linkProject(projectPath, options) {
1514
1710
  if (allSavedBytes > 0) {
1515
1711
  info(`本次节省: ${formatSize(allSavedBytes)}`);
1516
1712
  }
1517
- const totalSize = await store.getTotalSize();
1518
- info(`Store 总计: ${formatSize(totalSize)}`);
1519
- const unreferencedStores = registry.getUnreferencedStores();
1520
- if (unreferencedStores.length > 0) {
1521
- const unreferencedSize = unreferencedStores.reduce((sum, entry) => sum + entry.size, 0);
1522
- hint(`无引用: ${unreferencedStores.length} (${formatSize(unreferencedSize)}) - 可用 td clean 清理`);
1713
+ const spaceService = new SpaceService(registry, storePath);
1714
+ const spaceSnapshot = await spaceService.buildSnapshot('summary');
1715
+ info(`Store 总计: ${formatSize(spaceSnapshot.totals.physical.bytes)}`);
1716
+ const cleanupCandidates = spaceService.getCleanupCandidates(spaceSnapshot);
1717
+ const allCleanup = buildUnreferencedSelectionSummary(cleanupCandidates);
1718
+ if (allCleanup.platformCount > 0) {
1719
+ hint(`无引用: ${allCleanup.platformCount} 个 (约 ${formatSize(allCleanup.registeredBytes)}) - 可用 td clean 清理`);
1523
1720
  // capacity 策略:检查是否超过阈值
1524
1721
  const cleanStrategy = await config.get('cleanStrategy');
1525
1722
  if (cleanStrategy === 'capacity') {
1526
1723
  const threshold = (await config.get('unreferencedThreshold')) ?? 10 * 1024 * 1024 * 1024;
1527
- if (unreferencedSize > threshold) {
1724
+ if (allCleanup.registeredBytes > threshold) {
1528
1725
  blank();
1529
- warn(`无引用库容量 (${formatSize(unreferencedSize)}) 已超过阈值 (${formatSize(threshold)})`);
1726
+ warn(`无引用库容量 (${formatSize(allCleanup.registeredBytes)}) 已超过阈值 (${formatSize(threshold)})`);
1530
1727
  if (!options.yes && process.stdout.isTTY) {
1531
- // 交互模式:提供选项
1532
- const halfCleanStores = registry.getStoresForHalfClean();
1533
- const halfCleanSize = halfCleanStores.reduce((sum, e) => sum + e.size, 0);
1728
+ const halfCleanup = buildHalfCleanupSelectionSummary(cleanupCandidates);
1534
1729
  const action = await selectOption('选择清理方式:', [
1535
1730
  { name: '跳过本次', value: 'skip' },
1536
1731
  {
1537
- name: `清理最久未引用的 (释放约 ${formatSize(halfCleanSize)})`,
1732
+ name: `清理最久未引用的 (释放约 ${formatSize(halfCleanup.registeredBytes)})`,
1538
1733
  value: 'half',
1539
1734
  },
1540
- { name: `全部清理 (释放 ${formatSize(unreferencedSize)})`, value: 'all' },
1735
+ { name: `全部清理 (释放约 ${formatSize(allCleanup.registeredBytes)})`, value: 'all' },
1541
1736
  ]);
1542
1737
  // ESC 取消视为跳过
1543
1738
  if (action !== PROMPT_CANCELLED && action !== 'skip') {
1544
- const toClean = action === 'half' ? halfCleanStores : unreferencedStores;
1739
+ const cleanup = action === 'half' ? halfCleanup : allCleanup;
1740
+ const plan = await spaceService.buildCleanupPlan({
1741
+ reason: 'capacity',
1742
+ selections: cleanup.selections,
1743
+ requireUnreferenced: true,
1744
+ });
1745
+ for (const planWarning of plan.warnings)
1746
+ warn(`[space] ${planWarning}`);
1545
1747
  blank();
1546
- info('正在清理...');
1547
- let cleaned = 0;
1548
- let freedSize = 0;
1549
- for (const entry of toClean) {
1550
- try {
1551
- await store.remove(entry.libName, entry.commit, entry.platform);
1552
- const storeKey = registry.getStoreKey(entry.libName, entry.commit, entry.platform);
1553
- registry.removeStore(storeKey);
1554
- freedSize += entry.size;
1555
- cleaned++;
1556
- }
1557
- catch (cleanErr) {
1558
- warn(`清理 ${entry.libName}/${entry.platform} 失败: ${cleanErr.message}`);
1559
- }
1560
- }
1561
- await registry.save();
1562
- success(`清理完成: 删除 ${cleaned} 个库,释放 ${formatSize(freedSize)}`);
1748
+ info(`正在执行清理计划 ${plan.id}...`);
1749
+ const cleanupResult = await spaceService.createCleanupExecutor().execute(plan);
1750
+ success(`清理完成: 删除 ${cleanupResult.succeeded.length} 项,释放 ${formatSize(cleanupResult.released.bytes)}`);
1563
1751
  }
1564
1752
  }
1565
1753
  else {
@@ -1577,14 +1765,16 @@ export async function linkProject(projectPath, options) {
1577
1765
  // 链接过程出错,回滚事务
1578
1766
  error(`链接失败: ${err.message}`);
1579
1767
  hintStoreRepairCommand(err);
1580
- warn('正在回滚事务...');
1581
- try {
1582
- await tx.rollback();
1583
- success('事务已回滚');
1584
- }
1585
- catch (rollbackErr) {
1586
- error(`回滚失败: ${rollbackErr.message}`);
1587
- hint('请手动检查 Store 和项目目录状态');
1768
+ if (!options.dryRun) {
1769
+ warn('正在回滚事务...');
1770
+ try {
1771
+ await tx.rollback();
1772
+ success('事务已回滚');
1773
+ }
1774
+ catch (rollbackErr) {
1775
+ error(`回滚失败: ${rollbackErr.message}`);
1776
+ hint('请手动检查 Store 和项目目录状态');
1777
+ }
1588
1778
  }
1589
1779
  process.exit(1);
1590
1780
  }
@@ -1673,14 +1863,11 @@ async function ensureStoreInfo(libName, commit, platform, branch, url, projectHa
1673
1863
  lastAccess: new Date().toISOString(),
1674
1864
  });
1675
1865
  }
1676
- else if (existingEntry.fileCount == null) {
1677
- const integrity = await store.captureIntegrity(libName, commit, platform);
1678
- registry.updateStore(storeKey, integrity);
1679
- }
1680
- if (existingEntry && ((!existingEntry.branch && branch) || (!existingEntry.url && url))) {
1866
+ const currentEntry = registry.getStore(storeKey);
1867
+ if (currentEntry && ((!currentEntry.branch && branch) || (!currentEntry.url && url))) {
1681
1868
  registry.updateStore(storeKey, {
1682
- branch: existingEntry.branch || branch,
1683
- url: existingEntry.url || url,
1869
+ branch: currentEntry.branch || branch,
1870
+ url: currentEntry.url || url,
1684
1871
  lastAccess: new Date().toISOString(),
1685
1872
  });
1686
1873
  }
@@ -1895,7 +2082,7 @@ export async function classifyLinkStatus(localPath, storeCommitPath, isGeneral,
1895
2082
  * @param configPath 配置文件路径
1896
2083
  * @param platforms 请求的平台列表
1897
2084
  */
1898
- async function classifyDependencies(dependencies, projectPath, configPath, platforms, vars) {
2085
+ async function classifyDependencies(dependencies, projectPath, configPath, platforms, vars, repairStore = true) {
1899
2086
  const result = [];
1900
2087
  const thirdPartyDir = path.dirname(configPath);
1901
2088
  const storePath = await store.getStorePath();
@@ -1909,24 +2096,33 @@ async function classifyDependencies(dependencies, projectPath, configPath, platf
1909
2096
  const blockedPlatforms = getBlockedRequestedPlatforms(getRegistry(), dep, platforms, vars);
1910
2097
  const requestedPlatforms = platforms.filter((platform) => !blockedPlatforms.includes(platform));
1911
2098
  // 检查 Store 中是否有任意请求的平台(支持 sparse 承载目录)
1912
- const { actualExisting: rawExisting, satisfiedRequested, } = await resolveRequestedStoreState(dep, requestedPlatforms, vars);
2099
+ const { actualExisting: rawExisting, satisfiedRequested, } = await resolveRequestedStoreState(dep, requestedPlatforms, vars, repairStore);
2100
+ const registry = getRegistry();
2101
+ const libKey = registry.getLibraryKey(dep.libName, dep.commit);
2102
+ const library = registry.getLibrary(libKey);
1913
2103
  // 完整性校验:验证 existing 平台的文件完整性
1914
- const { valid: verifiedExisting, corrupted } = await verifyExistingPlatforms(dep.libName, dep.commit, rawExisting);
2104
+ const { valid: verifiedExisting, corrupted, repairRequired } = await verifyExistingPlatforms(dep.libName, dep.commit, rawExisting);
1915
2105
  // 清除损坏的平台数据
1916
- if (corrupted.length > 0) {
1917
- const registry = getRegistry();
1918
- for (const platform of corrupted) {
1919
- const storeKey = registry.getStoreKey(dep.libName, dep.commit, platform);
1920
- registry.removeStore(storeKey);
1921
- const platformPath = store.getLibraryPath(storePath, dep.libName, dep.commit, platform);
1922
- await fs.rm(platformPath, { recursive: true, force: true }).catch(() => { });
2106
+ if (repairStore && (corrupted.length > 0 || repairRequired.length > 0)) {
2107
+ await executeLinkStoreRepair(dep, [...new Set([...corrupted, ...repairRequired])], { category: 'classified-platform-corruption' });
2108
+ }
2109
+ let existing = verifiedExisting;
2110
+ const repairRequiredPlatforms = [...repairRequired];
2111
+ if (library?.isGeneral === true &&
2112
+ !(await hasRequestedPlatformContent(dep, requestedPlatforms, vars))) {
2113
+ if (repairStore)
2114
+ await prepareGeneralStoreRepair(dep, repairRequiredPlatforms);
2115
+ }
2116
+ if (repairStore && existing.length > 0) {
2117
+ const sharedRepairPlatforms = await prepareSharedStoreRepair(dep, existing, repairRequiredPlatforms);
2118
+ if (sharedRepairPlatforms.length > 0) {
2119
+ existing = existing.filter((platform) => !sharedRepairPlatforms.includes(platform));
1923
2120
  }
1924
2121
  }
1925
- const existing = verifiedExisting;
1926
2122
  // 也检查是否为 General 库(有 _shared 且有内容)
1927
- const isGeneral = await resolveLibraryGeneralState(dep, requestedPlatforms, vars);
2123
+ const isGeneral = await resolveLibraryGeneralState(dep, requestedPlatforms, vars, repairStore);
1928
2124
  const inStore = existing.length > 0 || isGeneral;
1929
- if (satisfiedRequested.length > 0) {
2125
+ if (repairStore && satisfiedRequested.length > 0) {
1930
2126
  clearSatisfiedAutoUnavailablePlatforms(getRegistry(), dep, satisfiedRequested);
1931
2127
  }
1932
2128
  // 用于非 inStore 情况的本地路径状态检查
@@ -2004,6 +2200,7 @@ async function classifyDependencies(dependencies, projectPath, configPath, platf
2004
2200
  status,
2005
2201
  localPath,
2006
2202
  storePath: storeLibPath,
2203
+ repairRequiredPlatforms,
2007
2204
  });
2008
2205
  }
2009
2206
  return result;
@@ -2012,6 +2209,7 @@ async function supplementMissingPlatforms(dependency, platforms, registry, tx, o
2012
2209
  const result = { downloaded: [], unavailable: [], nestedLibraries: [] };
2013
2210
  const blockedPlatforms = getBlockedRequestedPlatforms(registry, dependency, platforms, options.vars);
2014
2211
  const requestedPlatforms = platforms.filter((platform) => !blockedPlatforms.includes(platform));
2212
+ const repairRequiredPlatforms = new Set(options.repairRequiredPlatforms ?? []);
2015
2213
  // 1. 检查平台完整性
2016
2214
  const { missingRequested, satisfiedRequested } = await resolveRequestedStoreState(dependency, requestedPlatforms, options.vars);
2017
2215
  const missing = missingRequested;
@@ -2025,6 +2223,7 @@ async function supplementMissingPlatforms(dependency, platforms, registry, tx, o
2025
2223
  const toDownload = options.forceDownload
2026
2224
  ? missing
2027
2225
  : missing.filter((p) => !unavailablePlatforms.includes(p));
2226
+ const repairDownloadTargets = toDownload.filter((platform) => repairRequiredPlatforms.has(platform));
2028
2227
  if (toDownload.length === 0) {
2029
2228
  // 所有缺失平台都是已知不可用的
2030
2229
  if (missing.length > 0) {
@@ -2034,7 +2233,12 @@ async function supplementMissingPlatforms(dependency, platforms, registry, tx, o
2034
2233
  return result;
2035
2234
  }
2036
2235
  // 4. 尝试下载
2037
- info(`${dependency.libName} 缺少平台 [${toDownload.join(', ')}],尝试下载...`);
2236
+ if (repairDownloadTargets.length > 0) {
2237
+ debug(`${dependency.libName} ${repairDownloadTargets.join(', ')} 本地缓存不完整,开始重新下载`);
2238
+ }
2239
+ else {
2240
+ info(`${dependency.libName} 缺少平台 [${toDownload.join(', ')}],尝试下载...`);
2241
+ }
2038
2242
  try {
2039
2243
  const downloadResult = await codepac.downloadToTemp({
2040
2244
  url: dependency.url,
@@ -2056,6 +2260,10 @@ async function supplementMissingPlatforms(dependency, platforms, registry, tx, o
2056
2260
  const downloaded = assessment.downloadedRequested;
2057
2261
  result.downloaded = downloaded;
2058
2262
  clearSatisfiedAutoUnavailablePlatforms(registry, dependency, assessment.satisfiedRequested);
2263
+ const repaired = downloaded.filter((platform) => repairRequiredPlatforms.has(platform));
2264
+ if (repaired.length > 0) {
2265
+ warn(`${dependency.libName} ${repaired.join(', ')} 本地缓存不完整,已重新下载`);
2266
+ }
2059
2267
  // 6. 记录新发现的不可用平台
2060
2268
  const notFound = assessment.unavailableRequested;
2061
2269
  if (notFound.length > 0) {
@@ -2095,7 +2303,9 @@ async function supplementMissingPlatforms(dependency, platforms, registry, tx, o
2095
2303
  }
2096
2304
  // 注册 _shared 目录(如果存在)
2097
2305
  await registerSharedStore(dependency.libName, dependency.commit, dependency.branch, dependency.url);
2098
- success(`${dependency.libName} 已补充平台 [${downloaded.join(', ')}]`);
2306
+ if (repaired.length === 0) {
2307
+ success(`${dependency.libName} 已补充平台 [${downloaded.join(', ')}]`);
2308
+ }
2099
2309
  }
2100
2310
  }
2101
2311
  finally {
@@ -2104,7 +2314,16 @@ async function supplementMissingPlatforms(dependency, platforms, registry, tx, o
2104
2314
  }
2105
2315
  }
2106
2316
  catch (err) {
2107
- warn(`${dependency.libName} 下载失败: ${err.message}`);
2317
+ const repairTargets = toDownload.filter((platform) => repairRequiredPlatforms.has(platform));
2318
+ if (repairTargets.length > 0) {
2319
+ error(`${dependency.libName} ${repairTargets.join(', ')} 本地缓存不完整,自动重新下载失败`);
2320
+ hint(`可运行 td reset ${dependency.libName} 后重试`);
2321
+ debug(`${dependency.libName} 下载失败: ${err.message}`);
2322
+ throw err;
2323
+ }
2324
+ else {
2325
+ warn(`${dependency.libName} 下载失败: ${err.message}`);
2326
+ }
2108
2327
  }
2109
2328
  return result;
2110
2329
  }
@@ -2358,6 +2577,7 @@ async function linkNestedDependencies(dependencies, params) {
2358
2577
  const knownUnavailable = platforms.filter(p => unavailablePlatforms.includes(p));
2359
2578
  let storeHas = false;
2360
2579
  let existingPlatforms = [];
2580
+ let repairRequiredPlatforms = [];
2361
2581
  const initialStoreState = await resolveRequestedStoreState(dep, availablePlatforms, vars);
2362
2582
  existingPlatforms.push(...initialStoreState.actualExisting);
2363
2583
  if (initialStoreState.satisfiedRequested.length > 0) {
@@ -2366,15 +2586,27 @@ async function linkNestedDependencies(dependencies, params) {
2366
2586
  storeHas = existingPlatforms.length > 0;
2367
2587
  // 完整性校验:验证 existing 平台的文件完整性(与 classifyDependencies 同逻辑)
2368
2588
  if (existingPlatforms.length > 0) {
2369
- const { valid: verifiedPlatforms, corrupted } = await verifyExistingPlatforms(dep.libName, dep.commit, existingPlatforms);
2589
+ const { valid: verifiedPlatforms, corrupted, repairRequired } = await verifyExistingPlatforms(dep.libName, dep.commit, existingPlatforms);
2370
2590
  existingPlatforms = verifiedPlatforms;
2371
- if (corrupted.length > 0) {
2372
- for (const p of corrupted) {
2373
- const storeKey = registry.getStoreKey(dep.libName, dep.commit, p);
2374
- registry.removeStore(storeKey);
2375
- const platformPath = store.getLibraryPath(await store.getStorePath(), dep.libName, dep.commit, p);
2376
- await fs.rm(platformPath, { recursive: true, force: true }).catch(() => { });
2377
- }
2591
+ repairRequiredPlatforms = repairRequired;
2592
+ if (corrupted.length > 0 || repairRequired.length > 0) {
2593
+ await executeLinkStoreRepair(dep, [...new Set([...corrupted, ...repairRequired])], { category: 'nested-platform-corruption' });
2594
+ }
2595
+ storeHas = existingPlatforms.length > 0;
2596
+ }
2597
+ const nestedLibKey = registry.getLibraryKey(dep.libName, dep.commit);
2598
+ const nestedLibrary = registry.getLibrary(nestedLibKey);
2599
+ if (nestedLibrary?.isGeneral === true &&
2600
+ !(await hasRequestedPlatformContent(dep, availablePlatforms, vars))) {
2601
+ const generalNeedsRepair = await prepareGeneralStoreRepair(dep, repairRequiredPlatforms);
2602
+ if (generalNeedsRepair) {
2603
+ storeHas = existingPlatforms.length > 0;
2604
+ }
2605
+ }
2606
+ if (existingPlatforms.length > 0) {
2607
+ const sharedRepairPlatforms = await prepareSharedStoreRepair(dep, existingPlatforms, repairRequiredPlatforms);
2608
+ if (sharedRepairPlatforms.length > 0) {
2609
+ existingPlatforms = existingPlatforms.filter((platform) => !sharedRepairPlatforms.includes(platform));
2378
2610
  }
2379
2611
  storeHas = existingPlatforms.length > 0;
2380
2612
  }
@@ -2468,7 +2700,7 @@ async function linkNestedDependencies(dependencies, params) {
2468
2700
  let linkedPlatforms = isGeneral ? [GENERAL_PLATFORM] : [...existingPlatforms];
2469
2701
  // 已链接,但需要检查是否需要补充缺失平台(与顶层依赖逻辑一致)
2470
2702
  if (!isGeneral) {
2471
- const supplementResult = await supplementMissingPlatforms(dep, platforms, registry, tx, { vars, useGitLightweightDownload: options.gitLightweightDownload });
2703
+ const supplementResult = await supplementMissingPlatforms(dep, platforms, registry, tx, { vars, useGitLightweightDownload: options.gitLightweightDownload, repairRequiredPlatforms });
2472
2704
  // 注册嵌套依赖
2473
2705
  if (supplementResult.nestedLibraries.length > 0) {
2474
2706
  await registerNestedLibraries(supplementResult.nestedLibraries, projectHash);
@@ -2539,7 +2771,7 @@ async function linkNestedDependencies(dependencies, params) {
2539
2771
  }
2540
2772
  else {
2541
2773
  // 平台库:先补充缺失平台(与顶层依赖逻辑一致)
2542
- const supplementResult = await supplementMissingPlatforms(dep, platforms, registry, tx, { vars, useGitLightweightDownload: options.gitLightweightDownload });
2774
+ const supplementResult = await supplementMissingPlatforms(dep, platforms, registry, tx, { vars, useGitLightweightDownload: options.gitLightweightDownload, repairRequiredPlatforms });
2543
2775
  // 注册嵌套依赖
2544
2776
  if (supplementResult.nestedLibraries.length > 0) {
2545
2777
  await registerNestedLibraries(supplementResult.nestedLibraries, projectHash);
@@ -2565,7 +2797,13 @@ async function linkNestedDependencies(dependencies, params) {
2565
2797
  }
2566
2798
  else if (download) {
2567
2799
  // Store 没有,需要下载
2568
- info(`${indent} ${dep.libName} - 下载中...`);
2800
+ const repairTargets = repairRequiredPlatforms.filter((platform) => availablePlatforms.includes(platform) || platform === GENERAL_PLATFORM);
2801
+ if (repairTargets.length > 0) {
2802
+ debug(`${indent} ${dep.libName} ${repairTargets.join(', ')} 本地缓存不完整,开始重新下载`);
2803
+ }
2804
+ else {
2805
+ info(`${indent} ${dep.libName} - 下载中...`);
2806
+ }
2569
2807
  if (dryRun) {
2570
2808
  info(`${indent} ${dep.libName} - 将下载`);
2571
2809
  continue;
@@ -2623,7 +2861,12 @@ async function linkNestedDependencies(dependencies, params) {
2623
2861
  platform: GENERAL_PLATFORM,
2624
2862
  linkedPath: path.relative(projectRoot, localPath),
2625
2863
  });
2626
- success(`${indent} ${dep.libName} - 下载完成 (General)`);
2864
+ if (repairTargets.includes(GENERAL_PLATFORM)) {
2865
+ warn(`${indent} ${dep.libName} ${GENERAL_PLATFORM} 本地缓存不完整,已重新下载`);
2866
+ }
2867
+ else {
2868
+ success(`${indent} ${dep.libName} - 下载完成 (General)`);
2869
+ }
2627
2870
  }
2628
2871
  else if (assessment.downloadedRequested.length > 0) {
2629
2872
  // 平台库处理
@@ -2652,7 +2895,13 @@ async function linkNestedDependencies(dependencies, params) {
2652
2895
  });
2653
2896
  warn(`${indent} ${dep.libName} 平台 [${newUnavailable.join(', ')}] 远程不存在,已记录`);
2654
2897
  }
2655
- success(`${indent} ${dep.libName} - 下载完成 [${assessment.downloadedRequested.join(', ')}]`);
2898
+ const repaired = assessment.downloadedRequested.filter((platform) => repairRequiredPlatforms.includes(platform));
2899
+ if (repaired.length > 0) {
2900
+ warn(`${indent} ${dep.libName} ${repaired.join(', ')} 本地缓存不完整,已重新下载`);
2901
+ }
2902
+ else {
2903
+ success(`${indent} ${dep.libName} - 下载完成 [${assessment.downloadedRequested.join(', ')}]`);
2904
+ }
2656
2905
  }
2657
2906
  else {
2658
2907
  // 所有请求的平台都不可用,记录到 registry
@@ -2672,7 +2921,15 @@ async function linkNestedDependencies(dependencies, params) {
2672
2921
  }
2673
2922
  }
2674
2923
  catch (err) {
2675
- warn(`${indent} ${dep.libName} - 下载失败: ${err.message}`);
2924
+ if (repairTargets.length > 0) {
2925
+ error(`${indent} ${dep.libName} ${repairTargets.join(', ')} 本地缓存不完整,自动重新下载失败`);
2926
+ hint(`${indent} 可运行 td reset ${dep.libName} 后重试`);
2927
+ debug(`${indent} ${dep.libName} 下载失败: ${err.message}`);
2928
+ throw err;
2929
+ }
2930
+ else {
2931
+ warn(`${indent} ${dep.libName} - 下载失败: ${err.message}`);
2932
+ }
2676
2933
  }
2677
2934
  }
2678
2935
  else {
@@ -2703,73 +2960,37 @@ function mergeDepLists(main, optional) {
2703
2960
  return Array.from(depMap.values());
2704
2961
  }
2705
2962
  /**
2706
- * 校验已有平台的完整性
2707
- * 遍历 existing 平台列表,查询 Registry 中的 StoreEntry
2708
- * 如果有 fileCount,调用 quickVerify 校验;匹配则保留,不匹配则标记为 corrupted。
2709
- * 没有 fileCount 的条目跳过校验(视为合法)。
2963
+ * 校验已有平台的完整性。
2964
+ * 只有带 fileCount contentHash StoreEntry 才被视为可验证缓存。
2965
+ * 缺失完整性记录的旧缓存不能证明自身完整,会被标记为需要重新下载。
2710
2966
  *
2711
2967
  * @param libName 库名
2712
2968
  * @param commit commit hash
2713
2969
  * @param existingPlatforms Store 中已有的平台列表
2714
- * @returns { valid: string[]; corrupted: string[] } 合法和损坏的平台列表
2970
+ * @returns { valid: string[]; corrupted: string[]; repairRequired: string[] } 合法、损坏和需要重下的平台列表
2715
2971
  */
2716
2972
  export async function verifyExistingPlatforms(libName, commit, existingPlatforms) {
2717
2973
  const valid = [];
2718
2974
  const corrupted = [];
2975
+ const repairRequired = [];
2719
2976
  const registry = getRegistry();
2720
2977
  for (const platform of existingPlatforms) {
2721
2978
  const storeKey = registry.getStoreKey(libName, commit, platform);
2722
2979
  const entry = registry.getStore(storeKey);
2980
+ const visiblePlatform = platform === GENERAL_PLATFORM ? '_shared' : platform;
2723
2981
  if (!entry) {
2724
- try {
2725
- const integrity = await store.captureIntegrity(libName, commit, platform);
2726
- if (integrity.fileCount === 0) {
2727
- corrupted.push(platform);
2728
- warn(`${libName} (${commit.slice(0, 7)}) ${platform}: Store 目录为空`);
2729
- continue;
2730
- }
2731
- registry.addStore({
2732
- libName,
2733
- commit,
2734
- platform,
2735
- branch: '',
2736
- url: '',
2737
- ...integrity,
2738
- usedBy: [],
2739
- createdAt: new Date().toISOString(),
2740
- lastAccess: new Date().toISOString(),
2741
- });
2742
- }
2743
- catch {
2744
- corrupted.push(platform);
2745
- warn(`${libName} (${commit.slice(0, 7)}) ${platform}: Store 完整性回填失败`);
2746
- continue;
2747
- }
2748
- valid.push(platform);
2982
+ repairRequired.push(platform);
2983
+ debug(`${libName} (${commit.slice(0, 7)}) ${visiblePlatform}: Store 缺少 Registry 记录,准备重新下载`);
2749
2984
  continue;
2750
2985
  }
2751
2986
  if (entry.fileCount === 0) {
2752
2987
  corrupted.push(platform);
2753
- warn(`${libName} (${commit.slice(0, 7)}) ${platform}: Store 记录为空`);
2988
+ warn(`${libName} (${commit.slice(0, 7)}) ${visiblePlatform}: Store 记录为空`);
2754
2989
  continue;
2755
2990
  }
2756
- // 旧数据回填:升级后首次遇到无 fileCount 的条目,顺便记录完整性数据
2757
- if (entry.fileCount == null) {
2758
- try {
2759
- const integrity = await store.captureIntegrity(libName, commit, platform);
2760
- registry.updateStore(storeKey, integrity);
2761
- if (integrity.fileCount === 0) {
2762
- corrupted.push(platform);
2763
- warn(`${libName} (${commit.slice(0, 7)}) ${platform}: Store 目录为空`);
2764
- continue;
2765
- }
2766
- }
2767
- catch {
2768
- corrupted.push(platform);
2769
- warn(`${libName} (${commit.slice(0, 7)}) ${platform}: Store 完整性回填失败`);
2770
- continue;
2771
- }
2772
- valid.push(platform);
2991
+ if (entry.fileCount == null || !entry.contentHash) {
2992
+ repairRequired.push(platform);
2993
+ debug(`${libName} (${commit.slice(0, 7)}) ${visiblePlatform}: Store 缺少完整性记录,准备重新下载`);
2773
2994
  continue;
2774
2995
  }
2775
2996
  const result = await store.fullVerify(libName, commit, platform, {
@@ -2782,10 +3003,10 @@ export async function verifyExistingPlatforms(libName, commit, existingPlatforms
2782
3003
  }
2783
3004
  else {
2784
3005
  corrupted.push(platform);
2785
- warn(`${libName} (${commit.slice(0, 7)}) ${platform}: 完整性校验失败 - ${result.reason}`);
3006
+ warn(`${libName} (${commit.slice(0, 7)}) ${visiblePlatform}: 完整性校验失败 - ${result.reason}`);
2786
3007
  }
2787
3008
  }
2788
- return { valid, corrupted };
3009
+ return { valid, corrupted, repairRequired };
2789
3010
  }
2790
3011
  /**
2791
3012
  * 创建带完整性信息的 StoreEntry