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
@@ -7,13 +7,13 @@ import { Command } from 'commander';
7
7
  import { ensureInitialized } from '../core/guard.js';
8
8
  import * as config from '../core/config.js';
9
9
  import { setLogLevel } from '../utils/logger.js';
10
- import { parseProjectDependencies, getRelativeConfigPath, parseCodepacDep, extractActions, parseActionCommand, extractNestedDependencies, findAllCodepacConfigs, extractDependencies, resolveProjectRootPath, } from '../core/parser.js';
10
+ import { parseProjectDependencies, getRelativeConfigPath, parseCodepacDep, extractActions, buildActionExecutionPlan, extractNestedDependencies, findAllCodepacConfigs, extractDependencies, resolveProjectRootPath, } from '../core/parser.js';
11
11
  import { getRegistry } from '../core/registry.js';
12
12
  import * as store from '../core/store.js';
13
13
  import * as linker from '../core/linker.js';
14
14
  import * as codepac from '../core/codepac.js';
15
15
  import { setProxyConfig } from '../core/codepac.js';
16
- import { getPlatformHelpText, GENERAL_PLATFORM, SHARED_PLATFORM, pathsEqual, isSparseOnlyCommon, KNOWN_PLATFORM_VALUES, getRequestedPlatformTargets, } from '../core/platform.js';
16
+ import { getPlatformHelpText, GENERAL_PLATFORM, SHARED_PLATFORM, pathsEqual, isSparseOnlyCommon, KNOWN_PLATFORM_VALUES, getRequestedPlatformTargets, resolveSparseConfig, getBaseKeyForCodepac, } from '../core/platform.js';
17
17
  import { Transaction } from '../core/transaction.js';
18
18
  import { formatSize, checkDiskSpace } from '../utils/disk.js';
19
19
  import { getDirSize } from '../utils/fs-utils.js';
@@ -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';
@@ -37,6 +38,7 @@ export function createLinkCommand() {
37
38
  .option('--no-download', '不自动下载缺失库')
38
39
  .option('--dry-run', '只显示将要执行的操作')
39
40
  .option('--config <configs...>', '指定可选配置文件 (如 codepac-dep-inner.json)')
41
+ .option('--skip-action, --skip_action, -sa <names...>', '按 action.name 跳过指定嵌套 action')
40
42
  .option('--no-submodules', '不检测 git submodule 依赖')
41
43
  .addHelpText('after', `${getPlatformHelpText()}
42
44
 
@@ -48,6 +50,7 @@ export function createLinkCommand() {
48
50
  td link --dry-run 预览操作,不实际执行
49
51
  td link -y 跳过确认,自动执行
50
52
  td link --config codepac-dep-inner.json 使用指定的可选配置
53
+ td link --skip-action prepareNested 跳过指定 action.name
51
54
  td link --no-submodules 跳过 git submodule 检测`)
52
55
  .action(async (projectPath, options) => {
53
56
  await ensureInitialized();
@@ -73,6 +76,100 @@ function resolveRequestedPlatformsByActual(requestedPlatforms, sparse, actualPla
73
76
  missingRequested: requestedPlatforms.filter((platform) => !satisfiedRequested.includes(platform)),
74
77
  };
75
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
+ }
76
173
  export function assessDownloadResult(requestedPlatforms, sparse, downloadResult, vars) {
77
174
  const downloadedActual = new Set(downloadResult.platformDirs);
78
175
  const { satisfiedRequested, missingRequested, } = resolveRequestedPlatformsByActual(requestedPlatforms, sparse, downloadResult.platformDirs, vars);
@@ -90,15 +187,28 @@ export function assessDownloadResult(requestedPlatforms, sparse, downloadResult,
90
187
  isPureGeneral,
91
188
  };
92
189
  }
93
- async function resolveRequestedStoreState(dependency, requestedPlatforms, vars) {
190
+ async function resolveRequestedStoreState(dependency, requestedPlatforms, vars, repairMissingRecords = true) {
94
191
  const resolvedExisting = [];
192
+ const stalePlatforms = [];
95
193
  const candidatePlatforms = [...new Set(requestedPlatforms.flatMap((platform) => getRequestedPlatformTargets(platform, dependency.sparse, vars)))];
96
194
  for (const platform of candidatePlatforms) {
97
195
  if (await store.exists(dependency.libName, dependency.commit, platform)) {
98
196
  resolvedExisting.push(platform);
99
197
  }
198
+ else {
199
+ const registry = getRegistry();
200
+ const storeKey = registry.getStoreKey(dependency.libName, dependency.commit, platform);
201
+ if (registry.getStore(storeKey)) {
202
+ stalePlatforms.push(platform);
203
+ }
204
+ }
100
205
  }
101
- return resolveRequestedPlatformsByActual(requestedPlatforms, dependency.sparse, resolvedExisting, vars);
206
+ if (repairMissingRecords && stalePlatforms.length > 0) {
207
+ await executeLinkStoreRepair(dependency, stalePlatforms, { category: 'missing-platform-path' });
208
+ }
209
+ const result = resolveRequestedPlatformsByActual(requestedPlatforms, dependency.sparse, resolvedExisting, vars);
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(', ') || '无'}`);
211
+ return result;
102
212
  }
103
213
  function getBlockedRequestedPlatforms(registry, dependency, requestedPlatforms, vars) {
104
214
  const { manualPlatforms, autoPlatforms } = resolveUnavailablePlatforms(registry, dependency);
@@ -109,7 +219,9 @@ function getBlockedRequestedPlatforms(registry, dependency, requestedPlatforms,
109
219
  const targets = getRequestedPlatformTargets(platform, dependency.sparse, vars);
110
220
  return targets.length === 1 && targets[0] === platform;
111
221
  });
112
- return [...new Set([...manualBlocked, ...autoBlocked])];
222
+ const blocked = [...new Set([...manualBlocked, ...autoBlocked])];
223
+ debug(`[link-platforms] ${dependency.libName}@${dependency.commit.slice(0, 7)} 请求平台=${requestedPlatforms.join(', ') || '空'}, 手动跳过=${manualBlocked.join(', ') || '无'}, 自动跳过=${autoBlocked.join(', ') || '无'}, 最终跳过=${blocked.join(', ') || '无'}`);
224
+ return blocked;
113
225
  }
114
226
  function clearSatisfiedAutoUnavailablePlatforms(registry, dependency, satisfiedRequested) {
115
227
  if (satisfiedRequested.length === 0)
@@ -154,12 +266,80 @@ function upsertLibraryAvailability(dependency, updates) {
154
266
  lastAccess: new Date().toISOString(),
155
267
  });
156
268
  }
157
- export async function resolveLibraryGeneralState(dependency) {
269
+ export async function resolveLibraryGeneralState(dependency, requestedPlatforms = [], vars, updateRegistry = true) {
158
270
  const registry = getRegistry();
159
271
  const libKey = registry.getLibraryKey(dependency.libName, dependency.commit);
160
272
  const library = registry.getLibrary(libKey);
273
+ const sparseConfig = resolveSparseConfig(dependency.sparse, vars);
274
+ const hasPlatformSparse = sparseConfig
275
+ ? Object.keys(sparseConfig).some((key) => key !== 'common')
276
+ : Boolean(dependency.sparse && !isSparseOnlyCommon(dependency.sparse));
277
+ const hasPlatformLibraryHistory = requestedPlatforms.length > 0
278
+ && library?.isGeneral === false
279
+ && library.platforms.some((platform) => platform !== GENERAL_PLATFORM);
280
+ if (requestedPlatforms.length > 0) {
281
+ const requestedState = await resolveRequestedStoreState(dependency, requestedPlatforms, vars, updateRegistry);
282
+ if (requestedState.actualExisting.length > 0) {
283
+ if (updateRegistry && library?.isGeneral === true) {
284
+ registry.updateLibrary(libKey, {
285
+ platforms: requestedState.actualExisting,
286
+ isGeneral: false,
287
+ lastAccess: new Date().toISOString(),
288
+ });
289
+ }
290
+ return false;
291
+ }
292
+ }
293
+ if (requestedPlatforms.length > 0 && library?.isGeneral === true) {
294
+ if (await hasRequestedPlatformContent(dependency, requestedPlatforms, vars)) {
295
+ return false;
296
+ }
297
+ }
298
+ if (requestedPlatforms.length > 0 && hasPlatformSparse) {
299
+ if (updateRegistry && library?.isGeneral === true) {
300
+ registry.updateLibrary(libKey, {
301
+ isGeneral: false,
302
+ lastAccess: new Date().toISOString(),
303
+ });
304
+ }
305
+ return false;
306
+ }
307
+ if (hasPlatformLibraryHistory) {
308
+ return false;
309
+ }
161
310
  if (typeof library?.isGeneral === 'boolean') {
162
- return library.isGeneral;
311
+ if (library.isGeneral) {
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
+ }
323
+ return false;
324
+ }
325
+ const actualGeneral = await store.isGeneralLib(dependency.libName, dependency.commit);
326
+ if (updateRegistry && !actualGeneral) {
327
+ registry.updateLibrary(libKey, {
328
+ isGeneral: false,
329
+ lastAccess: new Date().toISOString(),
330
+ });
331
+ }
332
+ return actualGeneral;
333
+ }
334
+ const actualGeneral = await store.isGeneralLib(dependency.libName, dependency.commit);
335
+ if (updateRegistry && actualGeneral) {
336
+ registry.updateLibrary(libKey, {
337
+ platforms: [GENERAL_PLATFORM],
338
+ isGeneral: true,
339
+ lastAccess: new Date().toISOString(),
340
+ });
341
+ }
342
+ return actualGeneral;
163
343
  }
164
344
  return store.isGeneralLib(dependency.libName, dependency.commit);
165
345
  }
@@ -173,7 +353,7 @@ export function resolveUnavailablePlatforms(registry, dependency) {
173
353
  autoPlatforms,
174
354
  };
175
355
  }
176
- export async function collectProjectDependencyGraph(projectRoot) {
356
+ export async function collectProjectDependencyGraph(projectRoot, codepacPlatforms = []) {
177
357
  const { normalizedPath, configPath } = await resolveProjectRootPath(projectRoot);
178
358
  if (!configPath) {
179
359
  throw new Error('只能在项目目录下使用');
@@ -182,11 +362,17 @@ export async function collectProjectDependencyGraph(projectRoot) {
182
362
  const seenDeps = new Set();
183
363
  const visitedConfigs = new Set();
184
364
  const queue = [
185
- { configPath, source: 'direct' },
365
+ { configPath, targetDir: path.dirname(configPath), codepacPlatforms, source: 'direct' },
186
366
  ];
187
- const submodules = await findSubmoduleConfigs(normalizedPath);
367
+ const submodules = await findSubmoduleConfigs(normalizedPath, '', 0, codepacPlatforms);
188
368
  for (const submodule of submodules) {
189
- queue.push({ configPath: submodule.configPath, scope: submodule.relativePath, source: 'submodule' });
369
+ queue.push({
370
+ configPath: submodule.configPath,
371
+ targetDir: path.dirname(submodule.configPath),
372
+ codepacPlatforms,
373
+ scope: submodule.relativePath,
374
+ source: 'submodule',
375
+ });
190
376
  }
191
377
  while (queue.length > 0) {
192
378
  const current = queue.shift();
@@ -200,25 +386,32 @@ export async function collectProjectDependencyGraph(projectRoot) {
200
386
  catch {
201
387
  continue;
202
388
  }
203
- for (const dependency of extractDependencies(parsedConfig)) {
389
+ const extractOptions = { platforms: current.codepacPlatforms, configPath: current.configPath };
390
+ for (const dependency of extractDependencies(parsedConfig, extractOptions)) {
204
391
  const key = `${dependency.libName}:${dependency.commit}:${current.scope ?? ''}`;
205
392
  if (!seenDeps.has(key)) {
206
393
  seenDeps.add(key);
207
394
  results.push({ ...dependency, scope: current.scope, source: current.source });
208
395
  }
209
396
  }
210
- for (const action of extractActions(parsedConfig)) {
211
- let parsedAction;
397
+ for (const action of extractActions(parsedConfig, extractOptions)) {
398
+ let plan;
212
399
  try {
213
- parsedAction = parseActionCommand(action.command);
400
+ plan = buildActionExecutionPlan(action, {
401
+ parentConfigPath: current.configPath,
402
+ parentTargetDir: current.targetDir,
403
+ inheritedCodepacPlatforms: current.codepacPlatforms,
404
+ });
214
405
  }
215
406
  catch {
216
407
  continue;
217
408
  }
218
- const nestedConfigPath = path.resolve(path.dirname(current.configPath), parsedAction.configDir, 'codepac-dep.json');
219
409
  let nested;
220
410
  try {
221
- nested = await extractNestedDependencies(nestedConfigPath, parsedAction.libraries);
411
+ nested = await extractNestedDependencies(plan.nestedConfigPath, plan.libraries, {
412
+ platforms: plan.effectiveCodepacPlatforms,
413
+ configPath: plan.nestedConfigPath,
414
+ });
222
415
  }
223
416
  catch {
224
417
  continue;
@@ -230,7 +423,15 @@ export async function collectProjectDependencyGraph(projectRoot) {
230
423
  results.push({ ...dependency, scope: current.scope, source: 'nested' });
231
424
  }
232
425
  }
233
- queue.push({ configPath: nestedConfigPath, scope: current.scope, source: 'nested' });
426
+ if (!plan.parsed.disableAction) {
427
+ queue.push({
428
+ configPath: plan.nestedConfigPath,
429
+ targetDir: plan.nestedTargetDir,
430
+ codepacPlatforms: plan.effectiveCodepacPlatforms,
431
+ scope: current.scope,
432
+ source: 'nested',
433
+ });
434
+ }
234
435
  }
235
436
  }
236
437
  return results;
@@ -276,6 +477,16 @@ async function selectSubmodules(configs, options, remembered) {
276
477
  }
277
478
  return selected.map(s => ({ ...s }));
278
479
  }
480
+ function buildSubmoduleOptionalConfigState(submodules) {
481
+ const state = {};
482
+ for (const submodule of submodules) {
483
+ const names = submodule.selectedOptionalConfigs?.map((config) => config.name) ?? [];
484
+ if (names.length > 0) {
485
+ state[submodule.relativePath] = names;
486
+ }
487
+ }
488
+ return Object.keys(state).length > 0 ? state : undefined;
489
+ }
279
490
  /**
280
491
  * 单个 scope(主项目或 submodule)的链接处理
281
492
  *
@@ -284,6 +495,7 @@ async function selectSubmodules(configs, options, remembered) {
284
495
  async function linkScope(params) {
285
496
  const { scopeName, configPath, platforms, scanExtraPlatforms, registry, tx, projectHash, projectRoot, storePath, download, dryRun, yes, concurrency, gitLightweightDownload, optionalConfigs, scope, } = params;
286
497
  let finalLinkPlatforms = [...params.finalLinkPlatforms];
498
+ const codepacPlatforms = [...new Set(platforms.map((platform) => getBaseKeyForCodepac(platform)))];
287
499
  // 记录 General 类型库
288
500
  const generalLibs = new Set();
289
501
  const downloadedLibs = [];
@@ -292,17 +504,32 @@ async function linkScope(params) {
292
504
  info(`分析 ${scopeName}: ${configPath}`);
293
505
  let dependencies;
294
506
  let configVars;
507
+ const selectedActions = [];
508
+ const cacheConfigPaths = [configPath];
295
509
  try {
296
- const result = await parseProjectDependencies(path.dirname(path.dirname(configPath)));
510
+ const result = await parseProjectDependencies(path.dirname(path.dirname(configPath)), {
511
+ platforms: codepacPlatforms,
512
+ });
297
513
  dependencies = result.dependencies;
298
514
  configVars = result.vars;
515
+ const topLevelConfig = await parseCodepacDep(configPath);
516
+ selectedActions.push(...extractActions(topLevelConfig, { platforms: codepacPlatforms, configPath }));
299
517
  // 合并可选配置依赖
300
518
  if (optionalConfigs && optionalConfigs.length > 0) {
301
519
  for (const optionalConfig of optionalConfigs) {
302
520
  try {
303
521
  const optionalResult = await parseCodepacDep(optionalConfig.path);
304
- const optionalDeps = extractDependencies(optionalResult);
522
+ const optionalDeps = extractDependencies(optionalResult, {
523
+ platforms: codepacPlatforms,
524
+ configPath: optionalConfig.path,
525
+ });
305
526
  dependencies = mergeDepLists(dependencies, optionalDeps);
527
+ configVars = { ...configVars, ...optionalResult.vars };
528
+ selectedActions.push(...extractActions(optionalResult, {
529
+ platforms: codepacPlatforms,
530
+ configPath: optionalConfig.path,
531
+ }));
532
+ cacheConfigPaths.push(optionalConfig.path);
306
533
  info(` + ${optionalConfig.name}: ${optionalDeps.length} 个依赖`);
307
534
  }
308
535
  catch (optErr) {
@@ -319,7 +546,7 @@ async function linkScope(params) {
319
546
  blank();
320
547
  // 2. 分类依赖
321
548
  const scopePath = path.dirname(path.dirname(configPath));
322
- const classified = await classifyDependencies(dependencies, scopePath, configPath, platforms, configVars);
549
+ const classified = await classifyDependencies(dependencies, scopePath, configPath, platforms, configVars, !dryRun);
323
550
  const stats = {
324
551
  linked: 0, relink: 0, replace: 0, absorb: 0, missing: 0, linkNew: 0,
325
552
  };
@@ -336,14 +563,31 @@ async function linkScope(params) {
336
563
  }
337
564
  // 4. 磁盘空间预检
338
565
  if (stats.missing > 0 && download) {
339
- const estimatedSize = stats.missing * 500 * 1024 * 1024;
340
- const spaceCheck = await checkDiskSpace(storePath, estimatedSize);
341
- if (!spaceCheck.sufficient) {
342
- error(`磁盘空间不足: 预计需要 ${formatSize(spaceCheck.required)},可用 ${formatSize(spaceCheck.available)}(含 1GB 安全余量)`);
343
- process.exit(EXIT_CODES.IOERR);
344
- }
345
- if (spaceCheck.available === 0) {
346
- warn('无法获取磁盘空间信息,继续执行');
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}`);
580
+ }
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
+ }
347
591
  }
348
592
  }
349
593
  // 5. 预扫描额外平台(仅主项目 scope)
@@ -457,17 +701,18 @@ async function linkScope(params) {
457
701
  }
458
702
  // 7. 逐个处理依赖(LINKED/RELINK/REPLACE/ABSORB/LINK_NEW)
459
703
  for (const item of classified) {
460
- const { dependency, status, localPath } = item;
704
+ const { dependency, status, localPath, repairRequiredPlatforms } = item;
461
705
  await store.ensureCompatibleStore(storePath, dependency.libName, dependency.commit);
462
706
  switch (status) {
463
707
  case DependencyStatus.LINKED: {
464
708
  const blockedPlatforms = getBlockedRequestedPlatforms(registry, dependency, platforms, configVars);
465
709
  const requestedPlatforms = platforms.filter((platform) => !blockedPlatforms.includes(platform));
466
- const isLinkedGeneral = await resolveLibraryGeneralState(dependency);
710
+ const isLinkedGeneral = await resolveLibraryGeneralState(dependency, requestedPlatforms, configVars);
467
711
  if (!isLinkedGeneral) {
468
712
  const supplementResult = await supplementMissingPlatforms(dependency, platforms, registry, tx, {
469
713
  vars: configVars,
470
714
  useGitLightweightDownload: gitLightweightDownload,
715
+ repairRequiredPlatforms,
471
716
  });
472
717
  await registerNestedLibraries(supplementResult.nestedLibraries, projectHash);
473
718
  if (supplementResult.downloaded.length > 0) {
@@ -489,7 +734,7 @@ async function linkScope(params) {
489
734
  const blockedPlatforms = getBlockedRequestedPlatforms(registry, dependency, platforms, configVars);
490
735
  const requestedPlatforms = platforms.filter((platform) => !blockedPlatforms.includes(platform));
491
736
  const relinkCommitPath = path.join(storePath, dependency.libName, dependency.commit);
492
- const isRelinkGeneral = await resolveLibraryGeneralState(dependency);
737
+ const isRelinkGeneral = await resolveLibraryGeneralState(dependency, requestedPlatforms, configVars);
493
738
  if (isRelinkGeneral) {
494
739
  tx.recordOp('unlink', localPath);
495
740
  await linker.unlink(localPath);
@@ -504,6 +749,7 @@ async function linkScope(params) {
504
749
  const relinkSupplementResult = await supplementMissingPlatforms(dependency, platforms, registry, tx, {
505
750
  vars: configVars,
506
751
  useGitLightweightDownload: gitLightweightDownload,
752
+ repairRequiredPlatforms,
507
753
  });
508
754
  await registerNestedLibraries(relinkSupplementResult.nestedLibraries, projectHash);
509
755
  const { actualExisting: relinkExisting, satisfiedRequested, } = await resolveRequestedStoreState(dependency, requestedPlatforms, configVars);
@@ -547,7 +793,7 @@ async function linkScope(params) {
547
793
  const requestedPlatforms = platforms.filter((platform) => !blockedPlatforms.includes(platform));
548
794
  const replaceSize = await getDirSize(localPath);
549
795
  const replaceCommitPath = path.join(storePath, dependency.libName, dependency.commit);
550
- const isReplaceGeneral = await resolveLibraryGeneralState(dependency);
796
+ const isReplaceGeneral = await resolveLibraryGeneralState(dependency, requestedPlatforms, configVars);
551
797
  if (isReplaceGeneral) {
552
798
  const sharedPath = path.join(replaceCommitPath, '_shared');
553
799
  tx.recordOp('replace', localPath, sharedPath);
@@ -561,6 +807,7 @@ async function linkScope(params) {
561
807
  const replaceSupplementResult = await supplementMissingPlatforms(dependency, platforms, registry, tx, {
562
808
  vars: configVars,
563
809
  useGitLightweightDownload: gitLightweightDownload,
810
+ repairRequiredPlatforms,
564
811
  });
565
812
  await registerNestedLibraries(replaceSupplementResult.nestedLibraries, projectHash);
566
813
  const { actualExisting: replaceExisting, satisfiedRequested, } = await resolveRequestedStoreState(dependency, requestedPlatforms, configVars);
@@ -670,6 +917,7 @@ async function linkScope(params) {
670
917
  const absorbSupplementResult = await supplementMissingPlatforms(dependency, platforms, registry, tx, {
671
918
  vars: configVars,
672
919
  useGitLightweightDownload: gitLightweightDownload,
920
+ repairRequiredPlatforms,
673
921
  });
674
922
  await registerNestedLibraries(absorbSupplementResult.nestedLibraries, projectHash);
675
923
  if (absorbSupplementResult.downloaded.length > 0) {
@@ -736,9 +984,20 @@ async function linkScope(params) {
736
984
  const blockedPlatforms = getBlockedRequestedPlatforms(registry, dependency, platforms, configVars);
737
985
  const requestedPlatforms = platforms.filter((platform) => !blockedPlatforms.includes(platform));
738
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);
739
990
  const linkNewLibId = `${dependency.libName}@${dependency.commit}`;
740
991
  if (missing.length > 0 && !skipAllDownloads && downloadConfirmedLibs.has(linkNewLibId)) {
741
- 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
+ }
742
1001
  const linkNewLibKey = registry.getLibraryKey(dependency.libName, dependency.commit);
743
1002
  const linkNewHistoryLib = registry.getLibrary(linkNewLibKey);
744
1003
  const linkNewMonitor = new DownloadMonitor({
@@ -754,6 +1013,17 @@ async function linkScope(params) {
754
1013
  onHeartbeat: (message) => { linkNewMonitor.heartbeat(message); },
755
1014
  });
756
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
+ }
757
1027
  finally {
758
1028
  await linkNewMonitor.stop();
759
1029
  }
@@ -768,6 +1038,10 @@ async function linkScope(params) {
768
1038
  tx.recordOp('absorb', linkNewCommitPath, downloadResult.libDir);
769
1039
  const linkNewAbsorbResult = await store.absorbLib(downloadResult.libDir, filteredDownloaded, dependency.libName, dependency.commit);
770
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
+ }
771
1045
  }
772
1046
  }
773
1047
  finally {
@@ -776,14 +1050,19 @@ async function linkScope(params) {
776
1050
  }
777
1051
  const { actualExisting: linkNewExisting, satisfiedRequested, } = await resolveRequestedStoreState(dependency, requestedPlatforms, configVars);
778
1052
  clearSatisfiedAutoUnavailablePlatforms(registry, dependency, satisfiedRequested);
779
- const isLinkNewGeneral = await resolveLibraryGeneralState(dependency);
1053
+ const isLinkNewGeneral = await resolveLibraryGeneralState(dependency, requestedPlatforms, configVars);
780
1054
  if (isLinkNewGeneral) {
781
1055
  const sharedPath = path.join(linkNewCommitPath, '_shared');
782
1056
  tx.recordOp('link', localPath, sharedPath);
783
1057
  await linker.linkGeneral(localPath, sharedPath);
784
1058
  await ensureLinkedRegistryState(dependency.libName, dependency.commit, dependency.branch, dependency.url, [GENERAL_PLATFORM], projectHash, true);
785
1059
  generalLibs.add(dependency.libName);
786
- 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
+ }
787
1066
  }
788
1067
  else if (linkNewExisting.length === 0) {
789
1068
  const { KNOWN_PLATFORM_VALUES } = await import('../core/platform.js');
@@ -833,21 +1112,47 @@ async function linkScope(params) {
833
1112
  const multiBarManager = isTTY ? new MultiBarManager() : null;
834
1113
  const downloadLimit = pLimit(concurrency);
835
1114
  const downloadTasks = toDownload.map((item) => downloadLimit(async () => {
836
- const { dependency, localPath } = item;
1115
+ const { dependency, localPath, repairRequiredPlatforms = [] } = item;
1116
+ const repairRequiredSet = new Set(repairRequiredPlatforms);
837
1117
  const pLog = {
838
1118
  info: (msg) => multiBarManager ? multiBarManager.log(`[info] ${msg}`) : info(msg),
839
1119
  success: (msg) => multiBarManager ? multiBarManager.log(`[ok] ${msg}`) : success(msg),
840
1120
  hint: (msg) => multiBarManager ? multiBarManager.log(`[hint] ${msg}`) : hint(msg),
841
1121
  warn: (msg) => multiBarManager ? multiBarManager.log(`[warn] ${msg}`) : warn(msg),
842
- error: (msg) => multiBarManager ? multiBarManager.log(`[error] ${msg}`) : error(msg),
1122
+ error: (msg) => multiBarManager ? multiBarManager.log(`[err] ${msg}`) : error(msg),
843
1123
  };
844
1124
  try {
845
1125
  const storeCommitPath = path.join(storePath, dependency.libName, dependency.commit);
846
1126
  const blockedPlatforms = getBlockedRequestedPlatforms(registry, dependency, platforms, configVars);
847
1127
  const requestedPlatforms = platforms.filter((platform) => !blockedPlatforms.includes(platform));
1128
+ if (requestedPlatforms.length === 0) {
1129
+ pLog.warn(`${dependency.libName} 请求平台 [${platforms.join(', ')}] 均已按规则跳过,没有可链接产物`);
1130
+ if (await linker.cleanupEmptyLocalDirectory(localPath)) {
1131
+ pLog.warn(`${dependency.libName} 已清理历史遗留的空本地目录`);
1132
+ }
1133
+ return {
1134
+ success: false,
1135
+ name: dependency.libName,
1136
+ skipped: true,
1137
+ skippedPlatforms: blockedPlatforms,
1138
+ unsupported: true,
1139
+ };
1140
+ }
848
1141
  const { actualExisting: existing, missingRequested: missing, satisfiedRequested, } = await resolveRequestedStoreState(dependency, requestedPlatforms, configVars);
849
1142
  clearSatisfiedAutoUnavailablePlatforms(registry, dependency, satisfiedRequested);
850
1143
  if (missing.length === 0) {
1144
+ if (existing.length === 0) {
1145
+ pLog.warn(`${dependency.libName} 请求平台 [${requestedPlatforms.join(', ')}] 在 Store 中没有可链接产物,已拒绝创建空目录`);
1146
+ if (await linker.cleanupEmptyLocalDirectory(localPath)) {
1147
+ pLog.warn(`${dependency.libName} 已清理历史遗留的空本地目录`);
1148
+ }
1149
+ return {
1150
+ success: false,
1151
+ name: dependency.libName,
1152
+ skipped: true,
1153
+ skippedPlatforms: requestedPlatforms,
1154
+ };
1155
+ }
851
1156
  pLog.info(`${dependency.libName} 所有平台已存在,直接链接...`);
852
1157
  tx.recordOp('link', localPath, storeCommitPath);
853
1158
  await linker.linkLib(localPath, storeCommitPath, existing);
@@ -864,9 +1169,22 @@ async function linkScope(params) {
864
1169
  if (knownUnavailable.length > 0) {
865
1170
  pLog.warn(`${dependency.libName} 平台 [${knownUnavailable.join(', ')}] 已按规则跳过`);
866
1171
  }
1172
+ if (await linker.cleanupEmptyLocalDirectory(localPath)) {
1173
+ pLog.warn(`${dependency.libName} 已清理历史遗留的空本地目录`);
1174
+ }
867
1175
  return { success: false, name: dependency.libName, skipped: true, skippedPlatforms: missing, unsupported: true };
868
1176
  }
869
- 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
+ }
870
1188
  const estimatedSize = historyLib?.size;
871
1189
  const downloadMonitor = new DownloadMonitor({
872
1190
  name: ` ${dependency.libName}`, estimatedSize, getDirSize,
@@ -904,7 +1222,15 @@ async function linkScope(params) {
904
1222
  await linker.linkGeneral(localPath, sharedPath);
905
1223
  await ensureLinkedRegistryState(dependency.libName, dependency.commit, dependency.branch, dependency.url, [GENERAL_PLATFORM], projectHash, true);
906
1224
  generalLibs.add(dependency.libName);
907
- 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
+ }
908
1234
  return { success: true, name: dependency.libName, downloadedPlatforms: [GENERAL_PLATFORM], skippedPlatforms: [], isGeneral: true };
909
1235
  }
910
1236
  const filteredDownloaded = assessment.downloadedRequested;
@@ -924,6 +1250,10 @@ async function linkScope(params) {
924
1250
  }
925
1251
  const linkPlatforms = [...existing, ...filteredDownloaded];
926
1252
  if (linkPlatforms.length === 0) {
1253
+ pLog.warn(`${dependency.libName} 下载后没有生成请求平台 [${requestedPlatforms.join(', ')}] 的可链接产物,已拒绝创建空目录`);
1254
+ if (await linker.cleanupEmptyLocalDirectory(localPath)) {
1255
+ pLog.warn(`${dependency.libName} 已清理历史遗留的空本地目录`);
1256
+ }
927
1257
  return {
928
1258
  success: false,
929
1259
  name: dependency.libName,
@@ -935,7 +1265,13 @@ async function linkScope(params) {
935
1265
  await linker.linkLib(localPath, storeCommitPath, linkPlatforms);
936
1266
  await ensureLinkedRegistryState(dependency.libName, dependency.commit, dependency.branch, dependency.url, linkPlatforms, projectHash, false);
937
1267
  const notLinkedPlatforms = resolveRequestedPlatformsByActual(requestedPlatforms, dependency.sparse, linkPlatforms, configVars).missingRequested;
938
- 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
+ }
939
1275
  return { success: true, name: dependency.libName, downloadedPlatforms: linkPlatforms, skippedPlatforms: notLinkedPlatforms };
940
1276
  }
941
1277
  finally {
@@ -943,8 +1279,22 @@ async function linkScope(params) {
943
1279
  }
944
1280
  }
945
1281
  catch (err) {
946
- pLog.error(`${dependency.libName} 下载失败: ${err.message}`);
947
- 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
+ };
948
1298
  }
949
1299
  }));
950
1300
  const results = await Promise.all(downloadTasks);
@@ -955,6 +1305,12 @@ async function linkScope(params) {
955
1305
  info(`下载完成: ${succeeded.length}/${toDownload.length} 个库`);
956
1306
  if (failed.length > 0)
957
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
+ }
958
1314
  const allSkipped = [];
959
1315
  for (const r of results) {
960
1316
  if ('skippedPlatforms' in r && r.skippedPlatforms && r.skippedPlatforms.length > 0) {
@@ -978,26 +1334,32 @@ async function linkScope(params) {
978
1334
  }
979
1335
  }
980
1336
  // 9. 处理嵌套依赖 (actions)
981
- const topLevelConfig = await parseCodepacDep(configPath);
982
- const actions = extractActions(topLevelConfig);
1337
+ const actions = selectedActions;
983
1338
  const nestedLinkedDeps = [];
984
1339
  if (actions.length > 0) {
985
1340
  blank();
986
1341
  separator();
987
1342
  info(`发现 ${actions.length} 个嵌套依赖配置`);
988
1343
  const nestedContext = {
989
- depth: 0, processedConfigs: new Set([configPath]), platforms, vars: configVars,
1344
+ depth: 0,
1345
+ processedConfigs: new Set([buildActionVisitKey(configPath, path.dirname(configPath))]),
1346
+ platforms,
1347
+ codepacPlatforms,
1348
+ vars: configVars,
990
1349
  };
991
1350
  const thirdPartyDir = path.dirname(configPath);
992
1351
  for (const action of actions) {
993
- await processAction(action, nestedContext, thirdPartyDir, {
1352
+ await processAction(action, nestedContext, configPath, thirdPartyDir, {
994
1353
  tx, registry, projectHash, projectRoot, dryRun, download, yes,
995
- gitLightweightDownload, generalLibs, downloadedLibs, nestedLinkedDeps,
1354
+ gitLightweightDownload, skipActions: new Set(params.skipActions ?? []),
1355
+ generalLibs, downloadedLibs, nestedLinkedDeps,
996
1356
  });
997
1357
  }
998
1358
  }
999
1359
  // 10. 同步 cache 文件
1000
- await syncCacheFile(configPath);
1360
+ for (const cacheConfigPath of cacheConfigPaths) {
1361
+ await syncCacheFile(cacheConfigPath);
1362
+ }
1001
1363
  // 11. 构建返回结果
1002
1364
  const linkedDeps = [];
1003
1365
  for (const item of classified) {
@@ -1025,7 +1387,10 @@ async function linkScope(params) {
1025
1387
  export async function linkProject(projectPath, options) {
1026
1388
  const { absolutePath, normalizedPath, configPath: discoveredConfigPath, } = await resolveProjectRootPath(projectPath);
1027
1389
  // === 阶段 1: 初始化 ===
1028
- 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
+ }
1029
1394
  if (cfg?.logLevel)
1030
1395
  setLogLevel(cfg.logLevel);
1031
1396
  if (cfg?.proxy)
@@ -1033,7 +1398,7 @@ export async function linkProject(projectPath, options) {
1033
1398
  const concurrency = cfg?.concurrency ?? 5;
1034
1399
  const gitLightweightDownload = cfg?.gitLightweightDownload ?? true;
1035
1400
  const registry = getRegistry();
1036
- await registry.load();
1401
+ await registry.load({ migrate: !options.dryRun });
1037
1402
  const existingProject = registry.getProjectByPath(normalizedPath);
1038
1403
  const rememberedPlatforms = existingProject?.platforms;
1039
1404
  // 确定平台列表
@@ -1070,9 +1435,10 @@ export async function linkProject(projectPath, options) {
1070
1435
  error(`路径不存在: ${absolutePath}`);
1071
1436
  process.exit(EXIT_CODES.NOINPUT);
1072
1437
  }
1073
- // 发现可选配置文件
1074
- const thirdpartyDir = path.join(normalizedPath, '3rdparty');
1075
- const configDiscovery = await findAllCodepacConfigs(thirdpartyDir);
1438
+ // 发现可选配置文件,扫描位置跟随实际主配置所在目录。
1439
+ const configDiscovery = discoveredConfigPath
1440
+ ? await findAllCodepacConfigs(path.dirname(discoveredConfigPath))
1441
+ : null;
1076
1442
  let selectedOptionalConfigs = [];
1077
1443
  if (configDiscovery && configDiscovery.optionalConfigs.length > 0) {
1078
1444
  if (options.config && options.config.length > 0) {
@@ -1119,14 +1485,21 @@ export async function linkProject(projectPath, options) {
1119
1485
  // === 阶段 2: Submodule 检测 ===
1120
1486
  let selectedSubmodules = [];
1121
1487
  if (options.submodules !== false) {
1122
- const submoduleConfigs = await findSubmoduleConfigs(normalizedPath);
1488
+ const codepacPlatforms = [...new Set(platforms.map((platform) => getBaseKeyForCodepac(platform)))];
1489
+ const submoduleConfigs = await findSubmoduleConfigs(normalizedPath, '', 0, codepacPlatforms);
1123
1490
  if (submoduleConfigs.length > 0) {
1124
1491
  selectedSubmodules = await selectSubmodules(submoduleConfigs, options, existingProject?.submodules);
1125
1492
  // 为选中的 submodule 处理可选配置
1126
1493
  for (const sub of selectedSubmodules) {
1127
- if (sub.optionalConfigs.length > 0 && !options.yes && process.stdout.isTTY) {
1494
+ if (sub.optionalConfigs.length === 0)
1495
+ continue;
1496
+ const rememberedConfigs = existingProject?.submoduleOptionalConfigs?.[sub.relativePath] ?? [];
1497
+ if (options.yes) {
1498
+ sub.selectedOptionalConfigs = sub.optionalConfigs.filter((configInfo) => rememberedConfigs.includes(configInfo.name));
1499
+ }
1500
+ else if (process.stdout.isTTY) {
1128
1501
  info(`${sub.name} 发现可选配置:`);
1129
- const selected = await selectOptionalConfigs(sub.optionalConfigs, { isTTY: true, specifiedConfigs: [] });
1502
+ const selected = await selectOptionalConfigs(sub.optionalConfigs, { isTTY: true, specifiedConfigs: rememberedConfigs });
1130
1503
  if (selected !== PROMPT_CANCELLED) {
1131
1504
  sub.selectedOptionalConfigs = selected;
1132
1505
  }
@@ -1158,24 +1531,37 @@ export async function linkProject(projectPath, options) {
1158
1531
  }
1159
1532
  }
1160
1533
  const finalPath = normalizedRoot;
1161
- // 检查是否有未完成的事务需要恢复
1162
- const pendingTx = await Transaction.findPending();
1163
- if (pendingTx) {
1164
- warn(`发现未完成的事务 (${pendingTx.id.slice(0, 8)})`);
1165
- info('正在尝试回滚...');
1166
- try {
1167
- await pendingTx.rollback();
1168
- success('事务回滚完成');
1169
- }
1170
- catch (err) {
1171
- error(`回滚失败: ${err.message}`);
1172
- 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
+ }
1173
1548
  }
1174
1549
  }
1550
+ else {
1551
+ const pendingTransactions = await Transaction.getPendingTransactions();
1552
+ debug(`[link] dry-run 跳过事务恢复与事务日志创建: pending=${pendingTransactions.length}`);
1553
+ }
1175
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
+ }
1176
1561
  const projectHash = registry.hashPath(finalPath);
1177
1562
  const tx = new Transaction(`link:${finalPath}`);
1178
- await tx.begin();
1563
+ if (!options.dryRun)
1564
+ await tx.begin();
1179
1565
  // === 阶段 4: 依次执行 linkScope ===
1180
1566
  try {
1181
1567
  // 主项目
@@ -1194,6 +1580,7 @@ export async function linkProject(projectPath, options) {
1194
1580
  concurrency,
1195
1581
  gitLightweightDownload,
1196
1582
  optionalConfigs: selectedOptionalConfigs,
1583
+ skipActions: options.skipAction ?? [],
1197
1584
  });
1198
1585
  const finalLinkPlatforms = mainResult.finalLinkPlatforms;
1199
1586
  // 如果 dry-run 模式,linkScope 内已显示信息
@@ -1219,6 +1606,7 @@ export async function linkProject(projectPath, options) {
1219
1606
  gitLightweightDownload,
1220
1607
  optionalConfigs: sub.selectedOptionalConfigs,
1221
1608
  scope: sub.relativePath,
1609
+ skipActions: options.skipAction ?? [],
1222
1610
  });
1223
1611
  }
1224
1612
  return;
@@ -1245,6 +1633,7 @@ export async function linkProject(projectPath, options) {
1245
1633
  gitLightweightDownload,
1246
1634
  optionalConfigs: sub.selectedOptionalConfigs,
1247
1635
  scope: sub.relativePath,
1636
+ skipActions: options.skipAction ?? [],
1248
1637
  });
1249
1638
  subResults.push(subResult);
1250
1639
  }
@@ -1269,6 +1658,7 @@ export async function linkProject(projectPath, options) {
1269
1658
  ? selectedOptionalConfigs.map(c => c.name) : undefined,
1270
1659
  submodules: selectedSubmodules.length > 0
1271
1660
  ? selectedSubmodules.map(s => s.relativePath) : undefined,
1661
+ submoduleOptionalConfigs: buildSubmoduleOptionalConfigState(selectedSubmodules),
1272
1662
  });
1273
1663
  // 更新 Store 引用关系
1274
1664
  const newStoreKeys = allLinkedDeps.map((d) => registry.getStoreKey(d.libName, d.commit, d.platform));
@@ -1320,52 +1710,44 @@ export async function linkProject(projectPath, options) {
1320
1710
  if (allSavedBytes > 0) {
1321
1711
  info(`本次节省: ${formatSize(allSavedBytes)}`);
1322
1712
  }
1323
- const totalSize = await store.getTotalSize();
1324
- info(`Store 总计: ${formatSize(totalSize)}`);
1325
- const unreferencedStores = registry.getUnreferencedStores();
1326
- if (unreferencedStores.length > 0) {
1327
- const unreferencedSize = unreferencedStores.reduce((sum, entry) => sum + entry.size, 0);
1328
- 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 清理`);
1329
1720
  // capacity 策略:检查是否超过阈值
1330
1721
  const cleanStrategy = await config.get('cleanStrategy');
1331
1722
  if (cleanStrategy === 'capacity') {
1332
1723
  const threshold = (await config.get('unreferencedThreshold')) ?? 10 * 1024 * 1024 * 1024;
1333
- if (unreferencedSize > threshold) {
1724
+ if (allCleanup.registeredBytes > threshold) {
1334
1725
  blank();
1335
- warn(`无引用库容量 (${formatSize(unreferencedSize)}) 已超过阈值 (${formatSize(threshold)})`);
1726
+ warn(`无引用库容量 (${formatSize(allCleanup.registeredBytes)}) 已超过阈值 (${formatSize(threshold)})`);
1336
1727
  if (!options.yes && process.stdout.isTTY) {
1337
- // 交互模式:提供选项
1338
- const halfCleanStores = registry.getStoresForHalfClean();
1339
- const halfCleanSize = halfCleanStores.reduce((sum, e) => sum + e.size, 0);
1728
+ const halfCleanup = buildHalfCleanupSelectionSummary(cleanupCandidates);
1340
1729
  const action = await selectOption('选择清理方式:', [
1341
1730
  { name: '跳过本次', value: 'skip' },
1342
1731
  {
1343
- name: `清理最久未引用的 (释放约 ${formatSize(halfCleanSize)})`,
1732
+ name: `清理最久未引用的 (释放约 ${formatSize(halfCleanup.registeredBytes)})`,
1344
1733
  value: 'half',
1345
1734
  },
1346
- { name: `全部清理 (释放 ${formatSize(unreferencedSize)})`, value: 'all' },
1735
+ { name: `全部清理 (释放约 ${formatSize(allCleanup.registeredBytes)})`, value: 'all' },
1347
1736
  ]);
1348
1737
  // ESC 取消视为跳过
1349
1738
  if (action !== PROMPT_CANCELLED && action !== 'skip') {
1350
- 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}`);
1351
1747
  blank();
1352
- info('正在清理...');
1353
- let cleaned = 0;
1354
- let freedSize = 0;
1355
- for (const entry of toClean) {
1356
- try {
1357
- await store.remove(entry.libName, entry.commit, entry.platform);
1358
- const storeKey = registry.getStoreKey(entry.libName, entry.commit, entry.platform);
1359
- registry.removeStore(storeKey);
1360
- freedSize += entry.size;
1361
- cleaned++;
1362
- }
1363
- catch (cleanErr) {
1364
- warn(`清理 ${entry.libName}/${entry.platform} 失败: ${cleanErr.message}`);
1365
- }
1366
- }
1367
- await registry.save();
1368
- 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)}`);
1369
1751
  }
1370
1752
  }
1371
1753
  else {
@@ -1383,14 +1765,16 @@ export async function linkProject(projectPath, options) {
1383
1765
  // 链接过程出错,回滚事务
1384
1766
  error(`链接失败: ${err.message}`);
1385
1767
  hintStoreRepairCommand(err);
1386
- warn('正在回滚事务...');
1387
- try {
1388
- await tx.rollback();
1389
- success('事务已回滚');
1390
- }
1391
- catch (rollbackErr) {
1392
- error(`回滚失败: ${rollbackErr.message}`);
1393
- 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
+ }
1394
1778
  }
1395
1779
  process.exit(1);
1396
1780
  }
@@ -1419,10 +1803,7 @@ function hintStoreRepairCommand(err) {
1419
1803
  async function registerSharedStore(libName, commit, branch = '', url = '') {
1420
1804
  const registry = getRegistry();
1421
1805
  const storeKey = registry.getStoreKey(libName, commit, SHARED_PLATFORM);
1422
- // 如果已存在,不重复注册
1423
- if (registry.getStore(storeKey)) {
1424
- return true;
1425
- }
1806
+ const existingEntry = registry.getStore(storeKey);
1426
1807
  // 检查 _shared 目录是否存在
1427
1808
  const storePath = await config.getStorePath();
1428
1809
  if (!storePath)
@@ -1440,17 +1821,27 @@ async function registerSharedStore(libName, commit, branch = '', url = '') {
1440
1821
  }
1441
1822
  // 计算 _shared 完整性信息并注册
1442
1823
  const integrity = await store.captureIntegrity(libName, commit, SHARED_PLATFORM);
1443
- registry.addStore({
1444
- libName,
1445
- commit,
1446
- platform: SHARED_PLATFORM,
1447
- branch,
1448
- url,
1449
- ...integrity,
1450
- usedBy: [], // _shared 不追踪引用,生命周期跟随平台
1451
- createdAt: new Date().toISOString(),
1452
- lastAccess: new Date().toISOString(),
1453
- });
1824
+ if (existingEntry) {
1825
+ registry.updateStore(storeKey, {
1826
+ branch: existingEntry.branch || branch,
1827
+ url: existingEntry.url || url,
1828
+ ...integrity,
1829
+ lastAccess: new Date().toISOString(),
1830
+ });
1831
+ }
1832
+ else {
1833
+ registry.addStore({
1834
+ libName,
1835
+ commit,
1836
+ platform: SHARED_PLATFORM,
1837
+ branch,
1838
+ url,
1839
+ ...integrity,
1840
+ usedBy: [], // _shared 不追踪引用,生命周期跟随平台
1841
+ createdAt: new Date().toISOString(),
1842
+ lastAccess: new Date().toISOString(),
1843
+ });
1844
+ }
1454
1845
  debug(`注册 _shared: ${libName}:${commit.slice(0, 8)} (${formatSize(integrity.size)})`);
1455
1846
  return true;
1456
1847
  }
@@ -1472,9 +1863,13 @@ async function ensureStoreInfo(libName, commit, platform, branch, url, projectHa
1472
1863
  lastAccess: new Date().toISOString(),
1473
1864
  });
1474
1865
  }
1475
- else if (existingEntry.fileCount == null) {
1476
- const integrity = await store.captureIntegrity(libName, commit, platform);
1477
- registry.updateStore(storeKey, integrity);
1866
+ const currentEntry = registry.getStore(storeKey);
1867
+ if (currentEntry && ((!currentEntry.branch && branch) || (!currentEntry.url && url))) {
1868
+ registry.updateStore(storeKey, {
1869
+ branch: currentEntry.branch || branch,
1870
+ url: currentEntry.url || url,
1871
+ lastAccess: new Date().toISOString(),
1872
+ });
1478
1873
  }
1479
1874
  if (projectHash) {
1480
1875
  registry.addStoreReference(storeKey, projectHash);
@@ -1687,7 +2082,7 @@ export async function classifyLinkStatus(localPath, storeCommitPath, isGeneral,
1687
2082
  * @param configPath 配置文件路径
1688
2083
  * @param platforms 请求的平台列表
1689
2084
  */
1690
- async function classifyDependencies(dependencies, projectPath, configPath, platforms, vars) {
2085
+ async function classifyDependencies(dependencies, projectPath, configPath, platforms, vars, repairStore = true) {
1691
2086
  const result = [];
1692
2087
  const thirdPartyDir = path.dirname(configPath);
1693
2088
  const storePath = await store.getStorePath();
@@ -1701,24 +2096,33 @@ async function classifyDependencies(dependencies, projectPath, configPath, platf
1701
2096
  const blockedPlatforms = getBlockedRequestedPlatforms(getRegistry(), dep, platforms, vars);
1702
2097
  const requestedPlatforms = platforms.filter((platform) => !blockedPlatforms.includes(platform));
1703
2098
  // 检查 Store 中是否有任意请求的平台(支持 sparse 承载目录)
1704
- 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);
1705
2103
  // 完整性校验:验证 existing 平台的文件完整性
1706
- const { valid: verifiedExisting, corrupted } = await verifyExistingPlatforms(dep.libName, dep.commit, rawExisting);
2104
+ const { valid: verifiedExisting, corrupted, repairRequired } = await verifyExistingPlatforms(dep.libName, dep.commit, rawExisting);
1707
2105
  // 清除损坏的平台数据
1708
- if (corrupted.length > 0) {
1709
- const registry = getRegistry();
1710
- for (const platform of corrupted) {
1711
- const storeKey = registry.getStoreKey(dep.libName, dep.commit, platform);
1712
- registry.removeStore(storeKey);
1713
- const platformPath = store.getLibraryPath(storePath, dep.libName, dep.commit, platform);
1714
- 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));
1715
2120
  }
1716
2121
  }
1717
- const existing = verifiedExisting;
1718
2122
  // 也检查是否为 General 库(有 _shared 且有内容)
1719
- const isGeneral = await resolveLibraryGeneralState(dep);
2123
+ const isGeneral = await resolveLibraryGeneralState(dep, requestedPlatforms, vars, repairStore);
1720
2124
  const inStore = existing.length > 0 || isGeneral;
1721
- if (satisfiedRequested.length > 0) {
2125
+ if (repairStore && satisfiedRequested.length > 0) {
1722
2126
  clearSatisfiedAutoUnavailablePlatforms(getRegistry(), dep, satisfiedRequested);
1723
2127
  }
1724
2128
  // 用于非 inStore 情况的本地路径状态检查
@@ -1796,6 +2200,7 @@ async function classifyDependencies(dependencies, projectPath, configPath, platf
1796
2200
  status,
1797
2201
  localPath,
1798
2202
  storePath: storeLibPath,
2203
+ repairRequiredPlatforms,
1799
2204
  });
1800
2205
  }
1801
2206
  return result;
@@ -1804,6 +2209,7 @@ async function supplementMissingPlatforms(dependency, platforms, registry, tx, o
1804
2209
  const result = { downloaded: [], unavailable: [], nestedLibraries: [] };
1805
2210
  const blockedPlatforms = getBlockedRequestedPlatforms(registry, dependency, platforms, options.vars);
1806
2211
  const requestedPlatforms = platforms.filter((platform) => !blockedPlatforms.includes(platform));
2212
+ const repairRequiredPlatforms = new Set(options.repairRequiredPlatforms ?? []);
1807
2213
  // 1. 检查平台完整性
1808
2214
  const { missingRequested, satisfiedRequested } = await resolveRequestedStoreState(dependency, requestedPlatforms, options.vars);
1809
2215
  const missing = missingRequested;
@@ -1817,6 +2223,7 @@ async function supplementMissingPlatforms(dependency, platforms, registry, tx, o
1817
2223
  const toDownload = options.forceDownload
1818
2224
  ? missing
1819
2225
  : missing.filter((p) => !unavailablePlatforms.includes(p));
2226
+ const repairDownloadTargets = toDownload.filter((platform) => repairRequiredPlatforms.has(platform));
1820
2227
  if (toDownload.length === 0) {
1821
2228
  // 所有缺失平台都是已知不可用的
1822
2229
  if (missing.length > 0) {
@@ -1826,7 +2233,12 @@ async function supplementMissingPlatforms(dependency, platforms, registry, tx, o
1826
2233
  return result;
1827
2234
  }
1828
2235
  // 4. 尝试下载
1829
- 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
+ }
1830
2242
  try {
1831
2243
  const downloadResult = await codepac.downloadToTemp({
1832
2244
  url: dependency.url,
@@ -1848,6 +2260,10 @@ async function supplementMissingPlatforms(dependency, platforms, registry, tx, o
1848
2260
  const downloaded = assessment.downloadedRequested;
1849
2261
  result.downloaded = downloaded;
1850
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
+ }
1851
2267
  // 6. 记录新发现的不可用平台
1852
2268
  const notFound = assessment.unavailableRequested;
1853
2269
  if (notFound.length > 0) {
@@ -1887,7 +2303,9 @@ async function supplementMissingPlatforms(dependency, platforms, registry, tx, o
1887
2303
  }
1888
2304
  // 注册 _shared 目录(如果存在)
1889
2305
  await registerSharedStore(dependency.libName, dependency.commit, dependency.branch, dependency.url);
1890
- success(`${dependency.libName} 已补充平台 [${downloaded.join(', ')}]`);
2306
+ if (repaired.length === 0) {
2307
+ success(`${dependency.libName} 已补充平台 [${downloaded.join(', ')}]`);
2308
+ }
1891
2309
  }
1892
2310
  }
1893
2311
  finally {
@@ -1896,7 +2314,16 @@ async function supplementMissingPlatforms(dependency, platforms, registry, tx, o
1896
2314
  }
1897
2315
  }
1898
2316
  catch (err) {
1899
- 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
+ }
1900
2327
  }
1901
2328
  return result;
1902
2329
  }
@@ -1953,19 +2380,20 @@ function getStatusKey(status) {
1953
2380
  async function syncCacheFile(configPath) {
1954
2381
  const configDir = path.dirname(configPath);
1955
2382
  const cacheDir = path.join(configDir, '.cache');
1956
- const cachePath = path.join(cacheDir, 'codepac-dep.json');
2383
+ const configFileName = path.basename(configPath);
2384
+ const cachePath = path.join(cacheDir, configFileName);
1957
2385
  try {
1958
2386
  // 确保 .cache 目录存在
1959
2387
  await fs.mkdir(cacheDir, { recursive: true });
1960
2388
  // 复制配置文件到 cache
1961
2389
  await fs.copyFile(configPath, cachePath);
1962
2390
  if (process.env.VERBOSE) {
1963
- info(`已同步 cache: ${path.basename(configPath)}`);
2391
+ info(`已同步 cache: ${configFileName} -> ${path.relative(configDir, cachePath)}`);
1964
2392
  }
1965
2393
  }
1966
2394
  catch (err) {
1967
2395
  // cache 同步失败不应阻塞主流程,仅警告
1968
- warn(`cache 同步失败: ${err.message}`);
2396
+ warn(`cache 同步失败: ${configFileName} -> ${path.relative(configDir, cachePath)}: ${err.message}`);
1969
2397
  }
1970
2398
  }
1971
2399
  /**
@@ -1975,78 +2403,98 @@ async function syncCacheFile(configPath) {
1975
2403
  * @param thirdPartyDir 3rdparty 目录路径
1976
2404
  * @param options 处理选项
1977
2405
  */
1978
- async function processAction(action, context, thirdPartyDir, options) {
2406
+ async function processAction(action, context, parentConfigPath, parentTargetDir, options) {
1979
2407
  const indent = ' '.repeat(context.depth);
1980
- // 1. 解析 action 命令
1981
- let parsed;
2408
+ if (action.name && options.skipActions.has(action.name)) {
2409
+ info(`${indent}跳过嵌套 action: name=${action.name}, 原因=skip-action`);
2410
+ debug(`${indent}CodePac action 跳过: name=${action.name}, command=${action.command}, skipActions=${Array.from(options.skipActions).join(',')}`);
2411
+ return;
2412
+ }
2413
+ // 1. 解析 action 命令和执行计划
2414
+ let plan;
1982
2415
  try {
1983
- parsed = parseActionCommand(action.command);
2416
+ plan = buildActionExecutionPlan(action, {
2417
+ parentConfigPath,
2418
+ parentTargetDir,
2419
+ inheritedCodepacPlatforms: context.codepacPlatforms ?? context.platforms,
2420
+ });
1984
2421
  }
1985
2422
  catch (err) {
1986
- warn(`${indent}无法解析 action: ${err.message}`);
2423
+ warn(`${indent}跳过嵌套 action: name=${action.name ?? '(未命名)'}, 原因=无法解析命令, message=${err.message}`);
2424
+ debug(`${indent}CodePac action 解析失败: command=${action.command}`);
1987
2425
  return;
1988
2426
  }
1989
- const libsDisplay = parsed.libraries.length > 0 ? parsed.libraries.join(', ') : '全部依赖';
1990
- info(`${indent}处理嵌套依赖: ${parsed.configDir} [${libsDisplay}]`);
1991
- // 2. 构建嵌套配置路径
1992
- const nestedConfigPath = path.join(thirdPartyDir, parsed.configDir, 'codepac-dep.json');
1993
- // 3. 循环检测
1994
- if (context.processedConfigs.has(nestedConfigPath)) {
1995
- warn(`${indent} 检测到循环依赖,跳过: ${parsed.configDir}`);
2427
+ const libsDisplay = plan.libraries.length > 0 ? plan.libraries.join(', ') : '全部依赖';
2428
+ info(`${indent}处理嵌套 action: name=${plan.actionName ?? '(未命名)'}, config=${plan.nestedConfigPath}, target=${plan.nestedTargetDir}, libs=[${libsDisplay}]`);
2429
+ debug(`${indent}CodePac action 执行计划: command=${plan.command}, parentConfig=${parentConfigPath}, parentTarget=${parentTargetDir}, parsedConfigDir=${plan.configDir}, parsedTargetDir=${plan.targetDir}, resolvedConfig=${plan.nestedConfigPath}, resolvedTarget=${plan.nestedTargetDir}, inheritedPlatforms=${formatActionPlatforms(plan.inheritedPlatforms)}, effectivePlatforms=${formatActionPlatforms(plan.effectiveCodepacPlatforms)}, explicitPlatform=${plan.parsed.hasExplicitPlatform}, fullgit=${plan.parsed.hasExplicitFullGit}, unshallow=${plan.parsed.hasExplicitUnshallow}, disable_sparse=${plan.parsed.hasExplicitDisableSparse}`);
2430
+ // 2. 循环检测
2431
+ const visitKey = buildActionVisitKey(plan.nestedConfigPath, plan.nestedTargetDir);
2432
+ if (context.processedConfigs.has(visitKey)) {
2433
+ warn(`${indent} 跳过嵌套 action: name=${plan.actionName ?? '(未命名)'}, 原因=循环配置, config=${plan.nestedConfigPath}, target=${plan.nestedTargetDir}`);
1996
2434
  return;
1997
2435
  }
1998
- context.processedConfigs.add(nestedConfigPath);
1999
- // 4. 检查配置文件是否存在
2436
+ context.processedConfigs.add(visitKey);
2437
+ // 3. 检查配置文件是否存在
2000
2438
  try {
2001
- await fs.access(nestedConfigPath);
2439
+ await fs.access(plan.nestedConfigPath);
2002
2440
  }
2003
2441
  catch {
2004
- warn(`${indent} 嵌套配置文件不存在: ${nestedConfigPath}`);
2005
- hint(`${indent} 请确保 ${parsed.configDir} 库已被下载`);
2442
+ warn(`${indent} 跳过嵌套 action: name=${plan.actionName ?? '(未命名)'}, 原因=配置不存在, config=${plan.nestedConfigPath}`);
2443
+ hint(`${indent} 请确保 ${plan.configDir} 库已被下载`);
2006
2444
  return;
2007
2445
  }
2008
- // 5. 提取指定库的依赖
2446
+ // 4. 提取指定库的依赖
2009
2447
  let nestedResult;
2010
2448
  try {
2011
- nestedResult = await extractNestedDependencies(nestedConfigPath, parsed.libraries);
2449
+ nestedResult = await extractNestedDependencies(plan.nestedConfigPath, plan.libraries, {
2450
+ platforms: plan.effectiveCodepacPlatforms,
2451
+ configPath: plan.nestedConfigPath,
2452
+ });
2012
2453
  }
2013
2454
  catch (err) {
2014
- warn(`${indent} 解析嵌套配置失败: ${err.message}`);
2455
+ warn(`${indent} 跳过嵌套 action: name=${plan.actionName ?? '(未命名)'}, 原因=解析配置失败, message=${err.message}`);
2015
2456
  return;
2016
2457
  }
2017
2458
  const { dependencies, vars, nestedActions } = nestedResult;
2018
2459
  if (dependencies.length === 0) {
2019
- warn(`${indent} ${parsed.configDir} 中未找到指定的库`);
2460
+ warn(`${indent} 跳过嵌套 action: name=${plan.actionName ?? '(未命名)'}, 原因=未找到依赖, libs=[${libsDisplay}], config=${plan.nestedConfigPath}`);
2020
2461
  return;
2021
2462
  }
2022
- info(`${indent} 找到 ${dependencies.length} 个嵌套依赖`);
2023
- // 6. 合并变量
2463
+ info(`${indent} 嵌套 action 命中 ${dependencies.length} 个依赖,继承平台: ${formatActionPlatforms(plan.inheritedPlatforms)}, 生效平台: ${formatActionPlatforms(plan.effectiveCodepacPlatforms)}`);
2464
+ // 5. 合并变量
2024
2465
  const mergedVars = { ...context.vars, ...vars };
2025
- // 7. 处理这些依赖(targetDir 指定嵌套依赖的目标目录)
2466
+ // 6. 处理这些依赖(targetDir 指定嵌套依赖的目标目录)
2026
2467
  await linkNestedDependencies(dependencies, {
2027
- thirdPartyDir,
2028
- targetDir: parsed.targetDir,
2029
- nestedConfigPath,
2030
- context: { ...context, vars: mergedVars },
2468
+ targetDir: plan.nestedTargetDir,
2469
+ nestedConfigPath: plan.nestedConfigPath,
2470
+ context: { ...context, codepacPlatforms: plan.effectiveCodepacPlatforms, vars: mergedVars },
2031
2471
  options,
2032
2472
  indent,
2033
2473
  });
2034
2474
  // 同步嵌套配置文件的 cache(兼容 checkValid.js 检测)
2035
- await syncCacheFile(nestedConfigPath);
2036
- // 8. 递归处理嵌套 actions(如果没有 disable_action)
2037
- // 注意:递归时 thirdPartyDir 应该更新为当前嵌套依赖的目标目录
2038
- if (!parsed.disableAction && nestedActions.length > 0) {
2475
+ await syncCacheFile(plan.nestedConfigPath);
2476
+ // 7. 递归处理嵌套 actions(如果没有 disable_action)
2477
+ if (!plan.parsed.disableAction && nestedActions.length > 0) {
2039
2478
  const nestedContext = {
2040
2479
  depth: context.depth + 1,
2041
2480
  processedConfigs: context.processedConfigs,
2042
2481
  platforms: context.platforms,
2482
+ codepacPlatforms: plan.effectiveCodepacPlatforms,
2043
2483
  vars: mergedVars,
2044
2484
  };
2045
- const nestedThirdPartyDir = path.join(thirdPartyDir, parsed.targetDir);
2046
2485
  for (const nestedAction of nestedActions) {
2047
- await processAction(nestedAction, nestedContext, nestedThirdPartyDir, options);
2486
+ await processAction(nestedAction, nestedContext, plan.nestedConfigPath, plan.nestedTargetDir, options);
2048
2487
  }
2049
2488
  }
2489
+ else if (plan.parsed.disableAction && nestedActions.length > 0) {
2490
+ info(`${indent} 跳过后续嵌套 action: name=${plan.actionName ?? '(未命名)'}, 原因=disable_action`);
2491
+ }
2492
+ }
2493
+ function formatActionPlatforms(platforms) {
2494
+ return platforms && platforms.length > 0 ? platforms.join(',') : 'all';
2495
+ }
2496
+ function buildActionVisitKey(configPath, targetDir) {
2497
+ return `${configPath}::${targetDir}`;
2050
2498
  }
2051
2499
  /**
2052
2500
  * 统一的本地目录验证+吸收流程
@@ -2104,9 +2552,7 @@ async function resolveLocalAndAbsorb(localPath, libName, commit, options) {
2104
2552
  * 链接嵌套依赖
2105
2553
  */
2106
2554
  async function linkNestedDependencies(dependencies, params) {
2107
- const { thirdPartyDir, targetDir, context, options, indent } = params;
2108
- // 计算嵌套依赖的实际目标目录
2109
- const nestedTargetDir = path.join(thirdPartyDir, targetDir);
2555
+ const { targetDir: nestedTargetDir, context, options, indent } = params;
2110
2556
  const { tx, registry, projectHash, projectRoot, dryRun, download, generalLibs, downloadedLibs, nestedLinkedDeps } = options;
2111
2557
  const { platforms, vars } = context;
2112
2558
  for (const dep of dependencies) {
@@ -2130,7 +2576,8 @@ async function linkNestedDependencies(dependencies, params) {
2130
2576
  const availablePlatforms = platforms.filter(p => !unavailablePlatforms.includes(p));
2131
2577
  const knownUnavailable = platforms.filter(p => unavailablePlatforms.includes(p));
2132
2578
  let storeHas = false;
2133
- const existingPlatforms = [];
2579
+ let existingPlatforms = [];
2580
+ let repairRequiredPlatforms = [];
2134
2581
  const initialStoreState = await resolveRequestedStoreState(dep, availablePlatforms, vars);
2135
2582
  existingPlatforms.push(...initialStoreState.actualExisting);
2136
2583
  if (initialStoreState.satisfiedRequested.length > 0) {
@@ -2139,18 +2586,31 @@ async function linkNestedDependencies(dependencies, params) {
2139
2586
  storeHas = existingPlatforms.length > 0;
2140
2587
  // 完整性校验:验证 existing 平台的文件完整性(与 classifyDependencies 同逻辑)
2141
2588
  if (existingPlatforms.length > 0) {
2142
- const { valid: verifiedPlatforms, corrupted } = await verifyExistingPlatforms(dep.libName, dep.commit, existingPlatforms);
2143
- if (corrupted.length > 0) {
2144
- for (const p of corrupted) {
2145
- const storeKey = registry.getStoreKey(dep.libName, dep.commit, p);
2146
- registry.removeStore(storeKey);
2147
- const platformPath = store.getLibraryPath(await store.getStorePath(), dep.libName, dep.commit, p);
2148
- await fs.rm(platformPath, { recursive: true, force: true }).catch(() => { });
2149
- }
2150
- storeHas = verifiedPlatforms.length > 0;
2589
+ const { valid: verifiedPlatforms, corrupted, repairRequired } = await verifyExistingPlatforms(dep.libName, dep.commit, existingPlatforms);
2590
+ existingPlatforms = verifiedPlatforms;
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;
2151
2604
  }
2152
2605
  }
2153
- let isGeneral = await resolveLibraryGeneralState(dep);
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));
2610
+ }
2611
+ storeHas = existingPlatforms.length > 0;
2612
+ }
2613
+ let isGeneral = await resolveLibraryGeneralState(dep, availablePlatforms, vars);
2154
2614
  // 如果所有平台都已知不可用,跳过
2155
2615
  if (availablePlatforms.length === 0 && knownUnavailable.length > 0 && !isGeneral) {
2156
2616
  warn(`${indent} ${dep.libName} - 平台 [${knownUnavailable.join(', ')}] 已按规则跳过`);
@@ -2171,10 +2631,21 @@ async function linkNestedDependencies(dependencies, params) {
2171
2631
  if (resolveResult.isGeneral) {
2172
2632
  isGeneral = true;
2173
2633
  }
2634
+ else if (resolveResult.absorbResult) {
2635
+ const absorbedPlatforms = resolveResult.absorbResult.platforms;
2636
+ const absorbedPlatformSet = new Set(absorbedPlatforms);
2637
+ const linkableAbsorbedPlatforms = [...new Set(availablePlatforms.flatMap((platform) => getRequestedPlatformTargets(platform, dep.sparse, vars)
2638
+ .filter((target) => absorbedPlatformSet.has(target))))];
2639
+ existingPlatforms = [...new Set([
2640
+ ...existingPlatforms,
2641
+ ...linkableAbsorbedPlatforms,
2642
+ ])];
2643
+ debug(`${indent} ${dep.libName} - 本地吸收后平台回填: requested=${availablePlatforms.join(', ') || '无'}, absorbed=${absorbedPlatforms.join(', ') || '无'}, linkable=${existingPlatforms.join(', ') || '无'}`);
2644
+ }
2174
2645
  if (resolveResult.absorbResult?.nestedLibraries.length) {
2175
2646
  await registerNestedLibraries(resolveResult.absorbResult.nestedLibraries, projectHash);
2176
2647
  }
2177
- storeHas = true;
2648
+ storeHas = isGeneral || existingPlatforms.length > 0;
2178
2649
  // 吸收为 General 后,检查是否实际需要平台内容
2179
2650
  // 本地可能只有部分文件(如只有 _shared 内容),被误分类为 General
2180
2651
  if (resolveResult.isGeneral && download) {
@@ -2229,7 +2700,7 @@ async function linkNestedDependencies(dependencies, params) {
2229
2700
  let linkedPlatforms = isGeneral ? [GENERAL_PLATFORM] : [...existingPlatforms];
2230
2701
  // 已链接,但需要检查是否需要补充缺失平台(与顶层依赖逻辑一致)
2231
2702
  if (!isGeneral) {
2232
- 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 });
2233
2704
  // 注册嵌套依赖
2234
2705
  if (supplementResult.nestedLibraries.length > 0) {
2235
2706
  await registerNestedLibraries(supplementResult.nestedLibraries, projectHash);
@@ -2300,7 +2771,7 @@ async function linkNestedDependencies(dependencies, params) {
2300
2771
  }
2301
2772
  else {
2302
2773
  // 平台库:先补充缺失平台(与顶层依赖逻辑一致)
2303
- 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 });
2304
2775
  // 注册嵌套依赖
2305
2776
  if (supplementResult.nestedLibraries.length > 0) {
2306
2777
  await registerNestedLibraries(supplementResult.nestedLibraries, projectHash);
@@ -2326,7 +2797,13 @@ async function linkNestedDependencies(dependencies, params) {
2326
2797
  }
2327
2798
  else if (download) {
2328
2799
  // Store 没有,需要下载
2329
- 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
+ }
2330
2807
  if (dryRun) {
2331
2808
  info(`${indent} ${dep.libName} - 将下载`);
2332
2809
  continue;
@@ -2384,7 +2861,12 @@ async function linkNestedDependencies(dependencies, params) {
2384
2861
  platform: GENERAL_PLATFORM,
2385
2862
  linkedPath: path.relative(projectRoot, localPath),
2386
2863
  });
2387
- 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
+ }
2388
2870
  }
2389
2871
  else if (assessment.downloadedRequested.length > 0) {
2390
2872
  // 平台库处理
@@ -2413,7 +2895,13 @@ async function linkNestedDependencies(dependencies, params) {
2413
2895
  });
2414
2896
  warn(`${indent} ${dep.libName} 平台 [${newUnavailable.join(', ')}] 远程不存在,已记录`);
2415
2897
  }
2416
- 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
+ }
2417
2905
  }
2418
2906
  else {
2419
2907
  // 所有请求的平台都不可用,记录到 registry
@@ -2433,7 +2921,15 @@ async function linkNestedDependencies(dependencies, params) {
2433
2921
  }
2434
2922
  }
2435
2923
  catch (err) {
2436
- 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
+ }
2437
2933
  }
2438
2934
  }
2439
2935
  else {
@@ -2464,53 +2960,53 @@ function mergeDepLists(main, optional) {
2464
2960
  return Array.from(depMap.values());
2465
2961
  }
2466
2962
  /**
2467
- * 校验已有平台的完整性
2468
- * 遍历 existing 平台列表,查询 Registry 中的 StoreEntry
2469
- * 如果有 fileCount,调用 quickVerify 校验;匹配则保留,不匹配则标记为 corrupted。
2470
- * 没有 fileCount 的条目跳过校验(视为合法)。
2963
+ * 校验已有平台的完整性。
2964
+ * 只有带 fileCount contentHash StoreEntry 才被视为可验证缓存。
2965
+ * 缺失完整性记录的旧缓存不能证明自身完整,会被标记为需要重新下载。
2471
2966
  *
2472
2967
  * @param libName 库名
2473
2968
  * @param commit commit hash
2474
2969
  * @param existingPlatforms Store 中已有的平台列表
2475
- * @returns { valid: string[]; corrupted: string[] } 合法和损坏的平台列表
2970
+ * @returns { valid: string[]; corrupted: string[]; repairRequired: string[] } 合法、损坏和需要重下的平台列表
2476
2971
  */
2477
2972
  export async function verifyExistingPlatforms(libName, commit, existingPlatforms) {
2478
2973
  const valid = [];
2479
2974
  const corrupted = [];
2975
+ const repairRequired = [];
2480
2976
  const registry = getRegistry();
2481
2977
  for (const platform of existingPlatforms) {
2482
2978
  const storeKey = registry.getStoreKey(libName, commit, platform);
2483
2979
  const entry = registry.getStore(storeKey);
2484
- // 没有 StoreEntry,跳过校验视为合法
2980
+ const visiblePlatform = platform === GENERAL_PLATFORM ? '_shared' : platform;
2485
2981
  if (!entry) {
2486
- valid.push(platform);
2982
+ repairRequired.push(platform);
2983
+ debug(`${libName} (${commit.slice(0, 7)}) ${visiblePlatform}: Store 缺少 Registry 记录,准备重新下载`);
2487
2984
  continue;
2488
2985
  }
2489
- // 旧数据回填:升级后首次遇到无 fileCount 的条目,顺便记录完整性数据
2490
- if (entry.fileCount == null) {
2491
- try {
2492
- const integrity = await store.captureIntegrity(libName, commit, platform);
2493
- registry.updateStore(storeKey, integrity);
2494
- }
2495
- catch {
2496
- // 回填失败不影响正常流程
2497
- }
2498
- valid.push(platform);
2986
+ if (entry.fileCount === 0) {
2987
+ corrupted.push(platform);
2988
+ warn(`${libName} (${commit.slice(0, 7)}) ${visiblePlatform}: Store 记录为空`);
2989
+ continue;
2990
+ }
2991
+ if (entry.fileCount == null || !entry.contentHash) {
2992
+ repairRequired.push(platform);
2993
+ debug(`${libName} (${commit.slice(0, 7)}) ${visiblePlatform}: Store 缺少完整性记录,准备重新下载`);
2499
2994
  continue;
2500
2995
  }
2501
- const result = await store.quickVerify(libName, commit, platform, {
2996
+ const result = await store.fullVerify(libName, commit, platform, {
2502
2997
  size: entry.size,
2503
2998
  fileCount: entry.fileCount,
2999
+ contentHash: entry.contentHash,
2504
3000
  });
2505
3001
  if (result.valid) {
2506
3002
  valid.push(platform);
2507
3003
  }
2508
3004
  else {
2509
3005
  corrupted.push(platform);
2510
- warn(`${libName} (${commit.slice(0, 7)}) ${platform}: 完整性校验失败 - ${result.reason}`);
3006
+ warn(`${libName} (${commit.slice(0, 7)}) ${visiblePlatform}: 完整性校验失败 - ${result.reason}`);
2511
3007
  }
2512
3008
  }
2513
- return { valid, corrupted };
3009
+ return { valid, corrupted, repairRequired };
2514
3010
  }
2515
3011
  /**
2516
3012
  * 创建带完整性信息的 StoreEntry