tanmi-dock 0.8.3 → 0.9.0

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.
@@ -13,13 +13,13 @@ 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 { resolvePath, getPlatformHelpText, GENERAL_PLATFORM, SHARED_PLATFORM, pathsEqual, isSparseOnlyCommon } from '../core/platform.js';
16
+ import { resolvePath, getPlatformHelpText, GENERAL_PLATFORM, SHARED_PLATFORM, pathsEqual, isSparseOnlyCommon, KNOWN_PLATFORM_VALUES } 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';
20
20
  import { ProgressTracker, DownloadMonitor, MultiBarManager } from '../utils/progress.js';
21
21
  import { success, warn, error, info, hint, blank, separator, debug } from '../utils/logger.js';
22
- import { verifyLocalCommit } from '../utils/git.js';
22
+ import { verifyLocalCommit, findSubmoduleConfigs } from '../utils/git.js';
23
23
  import { DependencyStatus } from '../types/index.js';
24
24
  import { withGlobalLock } from '../utils/global-lock.js';
25
25
  import { selectPlatforms, parsePlatformArgs, selectOption, selectOptionalConfigs, PROMPT_CANCELLED } from '../utils/prompt.js';
@@ -37,6 +37,7 @@ export function createLinkCommand() {
37
37
  .option('--no-download', '不自动下载缺失库')
38
38
  .option('--dry-run', '只显示将要执行的操作')
39
39
  .option('--config <configs...>', '指定可选配置文件 (如 codepac-dep-inner.json)')
40
+ .option('--no-submodules', '不检测 git submodule 依赖')
40
41
  .addHelpText('after', `${getPlatformHelpText()}
41
42
 
42
43
  示例:
@@ -46,7 +47,8 @@ export function createLinkCommand() {
46
47
  td link -p mac android 链接多个平台
47
48
  td link --dry-run 预览操作,不实际执行
48
49
  td link -y 跳过确认,自动执行
49
- td link --config codepac-dep-inner.json 使用指定的可选配置`)
50
+ td link --config codepac-dep-inner.json 使用指定的可选配置
51
+ td link --no-submodules 跳过 git submodule 检测`)
50
52
  .action(async (projectPath, options) => {
51
53
  await ensureInitialized();
52
54
  try {
@@ -59,120 +61,69 @@ export function createLinkCommand() {
59
61
  });
60
62
  }
61
63
  /**
62
- * 执行链接操作
64
+ * 选择要链接的 submodule
63
65
  */
64
- export async function linkProject(projectPath, options) {
65
- const absolutePath = resolvePath(projectPath);
66
- // 读取配置并应用
67
- const cfg = await config.load();
68
- if (cfg?.logLevel) {
69
- setLogLevel(cfg.logLevel);
70
- }
71
- if (cfg?.proxy) {
72
- setProxyConfig(cfg.proxy);
66
+ async function selectSubmodules(configs, options, remembered) {
67
+ if (configs.length === 0)
68
+ return [];
69
+ let selected;
70
+ if (options.yes) {
71
+ // --yes 模式:自动包含所有
72
+ selected = configs;
73
73
  }
74
- const concurrency = cfg?.concurrency ?? 5;
75
- // 获取项目之前的平台选择(用于记忆)
76
- const registry = getRegistry();
77
- await registry.load();
78
- const existingProject = registry.getProjectByPath(absolutePath);
79
- const rememberedPlatforms = existingProject?.platforms;
80
- // 确定平台列表
81
- let platforms;
82
- if (options.platform && options.platform.length > 0) {
83
- // CLI 指定了平台,解析为 values
84
- platforms = parsePlatformArgs(options.platform);
85
- }
86
- else if (!options.yes && process.stdout.isTTY) {
87
- // 交互模式:显示平台选择,使用记忆的平台作为默认勾选
88
- const selectedPlatforms = await selectPlatforms(rememberedPlatforms);
89
- // ESC 取消
90
- if (selectedPlatforms === PROMPT_CANCELLED) {
91
- info('已取消');
92
- return;
93
- }
94
- platforms = selectedPlatforms;
95
- if (platforms.length === 0) {
96
- error('至少需要选择一个平台');
97
- process.exit(EXIT_CODES.MISUSE);
74
+ else if (process.stdout.isTTY) {
75
+ // 交互模式:checkbox 选择
76
+ info('检测到 git submodule 包含依赖配置:');
77
+ blank();
78
+ const { checkboxWithCancel, PROMPT_CANCELLED } = await import('../utils/prompt.js');
79
+ const choices = configs.map(c => ({
80
+ name: `${c.name} (${c.depCount} 个库)`,
81
+ value: c.relativePath,
82
+ // 首次全选,之后使用记忆的选择
83
+ checked: remembered ? remembered.includes(c.relativePath) : true,
84
+ }));
85
+ const selectedPaths = await checkboxWithCancel({
86
+ message: '选择要一并链接的子模块:',
87
+ choices,
88
+ });
89
+ if (selectedPaths === PROMPT_CANCELLED) {
90
+ return [];
98
91
  }
92
+ selected = configs.filter(c => selectedPaths.includes(c.relativePath));
99
93
  }
100
94
  else {
101
- // 非交互模式且未指定平台,报错
102
- error('非交互模式下必须使用 -p 指定平台');
103
- hint('示例: tanmi-dock link -p mac ios');
95
+ // 非 TTY 且没有 --yes
96
+ error('发现 git submodule 依赖,非交互模式下必须使用 --yes 或 --no-submodules');
97
+ hint(`检测到子模块: ${configs.map(c => c.name).join(', ')}`);
98
+ hint('示例: td link --yes -p mac (包含所有子模块)');
99
+ hint(' td link --no-submodules -p mac (跳过子模块)');
104
100
  process.exit(EXIT_CODES.MISUSE);
105
101
  }
106
- // 检查项目路径
107
- try {
108
- const stat = await fs.stat(absolutePath);
109
- if (!stat.isDirectory()) {
110
- error(`路径不是目录: ${absolutePath}`);
111
- process.exit(EXIT_CODES.NOINPUT);
112
- }
113
- }
114
- catch {
115
- error(`路径不存在: ${absolutePath}`);
116
- process.exit(EXIT_CODES.NOINPUT);
117
- }
118
- // 发现可选配置文件(在 3rdparty 目录中查找)
119
- const thirdpartyDir = path.join(absolutePath, '3rdparty');
120
- const configDiscovery = await findAllCodepacConfigs(thirdpartyDir);
121
- let selectedOptionalConfigs = [];
122
- if (configDiscovery && configDiscovery.optionalConfigs.length > 0) {
123
- // 有可选配置
124
- if (options.config && options.config.length > 0) {
125
- // CLI 指定了配置文件名,按名称查找对应配置
126
- for (const configName of options.config) {
127
- const found = configDiscovery.optionalConfigs.find(c => c.name === configName);
128
- if (!found) {
129
- error(`找不到指定的配置: ${configName}`);
130
- hint(`可用配置: ${configDiscovery.optionalConfigs.map(c => c.name).join(', ')}`);
131
- process.exit(EXIT_CODES.MISUSE);
132
- }
133
- selectedOptionalConfigs.push(found);
134
- }
135
- }
136
- else if (options.yes) {
137
- // --yes 模式:跳过可选配置选择(无论 TTY 还是非 TTY)
138
- // selectedOptionalConfigs 保持为空
139
- }
140
- else if (process.stdout.isTTY) {
141
- // TTY 交互模式:显示配置选择
142
- const rememberedConfigs = existingProject?.optionalConfigs ?? [];
143
- const selectOptions = {
144
- isTTY: true,
145
- specifiedConfigs: rememberedConfigs,
146
- };
147
- const selected = await selectOptionalConfigs(configDiscovery.optionalConfigs, selectOptions);
148
- if (selected === PROMPT_CANCELLED) {
149
- info('已取消');
150
- return;
151
- }
152
- selectedOptionalConfigs = selected;
153
- }
154
- else {
155
- // 非 TTY 模式且没有 --yes 或 --config:必须指定 --config
156
- error('发现可选配置文件,非交互模式下必须使用 --config 或 --yes 参数');
157
- hint(`可用配置: ${configDiscovery.optionalConfigs.map(c => c.name).join(', ')}`);
158
- hint(`示例: td link --config ${configDiscovery.optionalConfigs[0].name}`);
159
- hint('或使用 --yes 跳过可选配置');
160
- process.exit(EXIT_CODES.MISUSE);
161
- }
162
- }
163
- // 解析依赖
164
- info(`分析 ${absolutePath}`);
102
+ return selected.map(s => ({ ...s }));
103
+ }
104
+ /**
105
+ * 单个 scope(主项目或 submodule)的链接处理
106
+ *
107
+ * 包含:解析依赖、分类、预扫描平台、下载确认、逐个处理、并行下载、嵌套 actions
108
+ */
109
+ async function linkScope(params) {
110
+ const { scopeName, configPath, platforms, scanExtraPlatforms, registry, tx, projectHash, projectRoot, storePath, download, dryRun, yes, concurrency, optionalConfigs, scope, } = params;
111
+ let finalLinkPlatforms = [...params.finalLinkPlatforms];
112
+ // 记录 General 类型库
113
+ const generalLibs = new Set();
114
+ const downloadedLibs = [];
115
+ let savedBytes = 0;
116
+ // 1. 解析依赖
117
+ info(`分析 ${scopeName}: ${configPath}`);
165
118
  let dependencies;
166
- let configPath;
167
119
  let configVars;
168
120
  try {
169
- const result = await parseProjectDependencies(absolutePath);
121
+ const result = await parseProjectDependencies(path.dirname(path.dirname(configPath)));
170
122
  dependencies = result.dependencies;
171
- configPath = result.configPath;
172
123
  configVars = result.vars;
173
- // 如果选择了可选配置,合并额外的依赖
174
- if (selectedOptionalConfigs.length > 0) {
175
- for (const optionalConfig of selectedOptionalConfigs) {
124
+ // 合并可选配置依赖
125
+ if (optionalConfigs && optionalConfigs.length > 0) {
126
+ for (const optionalConfig of optionalConfigs) {
176
127
  try {
177
128
  const optionalResult = await parseCodepacDep(optionalConfig.path);
178
129
  const optionalDeps = extractDependencies(optionalResult);
@@ -189,61 +140,28 @@ export async function linkProject(projectPath, options) {
189
140
  error(err.message);
190
141
  process.exit(EXIT_CODES.DATAERR);
191
142
  }
192
- // 规范化项目根目录:确保始终登记在包含 3rdparty 的目录
193
- const normalizedRoot = normalizeProjectRoot(absolutePath, configPath);
194
- const wasNormalized = normalizedRoot !== absolutePath;
195
- // 如果路径被规范化了(用户在 3rdparty 目录运行),需要处理迁移
196
- if (wasNormalized) {
197
- info(`项目根目录规范化: ${absolutePath} → ${normalizedRoot}`);
198
- // 检查旧路径是否在 registry 中
199
- const oldHash = registry.hashPath(absolutePath);
200
- const oldProject = registry.getProject(oldHash);
201
- if (oldProject) {
202
- // 迁移旧登记到新路径
203
- info('迁移旧的项目登记...');
204
- registry.removeProject(oldHash);
205
- }
206
- }
207
- // 检查是否存在指向同一配置文件的其他登记(清理历史脏数据)
208
- const absConfigPath = path.resolve(normalizedRoot, getRelativeConfigPath(normalizedRoot, configPath));
209
- const allProjects = registry.listProjects();
210
- for (const proj of allProjects) {
211
- if (pathsEqual(proj.path, normalizedRoot))
212
- continue;
213
- const projAbsConfig = path.resolve(proj.path, proj.configPath);
214
- if (pathsEqual(projAbsConfig, absConfigPath)) {
215
- info(`清理重复登记: ${proj.path}`);
216
- registry.removeProject(registry.hashPath(proj.path));
217
- }
218
- }
219
- // 使用规范化后的路径继续
220
- const finalPath = normalizedRoot;
221
143
  info(`找到 ${dependencies.length} 个依赖,平台: ${platforms.join(', ')}`);
222
144
  blank();
223
- // 分类依赖(检查所有请求的平台)
224
- const classified = await classifyDependencies(dependencies, finalPath, configPath, platforms);
225
- // 显示分类结果
145
+ // 2. 分类依赖
146
+ const scopePath = path.dirname(path.dirname(configPath));
147
+ const classified = await classifyDependencies(dependencies, scopePath, configPath, platforms);
226
148
  const stats = {
227
- linked: 0,
228
- relink: 0,
229
- replace: 0,
230
- absorb: 0,
231
- missing: 0,
232
- linkNew: 0,
149
+ linked: 0, relink: 0, replace: 0, absorb: 0, missing: 0, linkNew: 0,
233
150
  };
234
151
  for (const item of classified) {
235
152
  stats[getStatusKey(item.status)]++;
236
153
  }
237
- // 如果是 dry-run,只显示信息
238
- if (options.dryRun) {
154
+ // 3. dry-run
155
+ if (dryRun) {
239
156
  showDryRunInfo(classified, stats);
240
- return;
157
+ return {
158
+ linkedDeps: [], nestedLinkedDeps: [], generalLibs, downloadedLibs,
159
+ savedBytes: 0, finalLinkPlatforms, stats,
160
+ };
241
161
  }
242
- // 磁盘空间预检(针对需要下载的库)
243
- if (stats.missing > 0 && options.download) {
244
- // 估算下载所需空间(每个库估算 500MB)
162
+ // 4. 磁盘空间预检
163
+ if (stats.missing > 0 && download) {
245
164
  const estimatedSize = stats.missing * 500 * 1024 * 1024;
246
- const storePath = await store.getStorePath();
247
165
  const spaceCheck = await checkDiskSpace(storePath, estimatedSize);
248
166
  if (!spaceCheck.sufficient) {
249
167
  error(`磁盘空间不足: 预计需要 ${formatSize(spaceCheck.required)},可用 ${formatSize(spaceCheck.available)}(含 1GB 安全余量)`);
@@ -253,34 +171,9 @@ export async function linkProject(projectPath, options) {
253
171
  warn('无法获取磁盘空间信息,继续执行');
254
172
  }
255
173
  }
256
- // 检查是否有未完成的事务需要恢复
257
- const pendingTx = await Transaction.findPending();
258
- if (pendingTx) {
259
- warn(`发现未完成的事务 (${pendingTx.id.slice(0, 8)})`);
260
- info('正在尝试回滚...');
261
- try {
262
- await pendingTx.rollback();
263
- success('事务回滚完成');
264
- }
265
- catch (err) {
266
- error(`回滚失败: ${err.message}`);
267
- hint('请手动检查 Store 和项目目录状态');
268
- }
269
- }
270
- // 执行链接(registry 已在前面加载)
271
- let savedBytes = 0;
272
- const storePath = await store.getStorePath();
273
- const projectHash = registry.hashPath(finalPath);
274
- // 创建事务
275
- const tx = new Transaction(`link:${finalPath}`);
276
- await tx.begin();
277
- // 记录 General 类型库(用于最后生成 dependencies 时使用正确的 platform)
278
- const generalLibs = new Set();
279
- // 预扫描所有本地存在的依赖的额外平台,让用户选择要链接的平台
280
- let finalLinkPlatforms = platforms; // 默认为用户请求的平台
281
- if (!options.yes && process.stdout.isTTY) {
174
+ // 5. 预扫描额外平台(仅主项目 scope)
175
+ if (scanExtraPlatforms && !yes && process.stdout.isTTY) {
282
176
  const { KNOWN_PLATFORM_VALUES } = await import('../core/platform.js');
283
- // 收集所有本地存在的平台(去重)- 扫描所有有本地目录的依赖
284
177
  const allLocalPlatforms = new Set();
285
178
  for (const item of classified) {
286
179
  try {
@@ -293,10 +186,9 @@ export async function linkProject(projectPath, options) {
293
186
  .forEach(e => allLocalPlatforms.add(e.name));
294
187
  }
295
188
  catch {
296
- // 读取失败,跳过(目录不存在等情况)
189
+ // 读取失败,跳过
297
190
  }
298
191
  }
299
- // 检测额外平台
300
192
  const extraPlatforms = [...allLocalPlatforms].filter(p => !platforms.includes(p));
301
193
  if (extraPlatforms.length > 0) {
302
194
  info(`本地检测到额外平台: ${extraPlatforms.join(', ')}`);
@@ -306,15 +198,15 @@ export async function linkProject(projectPath, options) {
306
198
  const selectedPlatforms = await checkboxWithCancel({
307
199
  message: '选择要链接的平台 (未选择的将被删除):',
308
200
  choices: allAvailable.map(p => ({
309
- name: p,
310
- value: p,
311
- checked: platforms.includes(p), // 用户请求的默认勾选
201
+ name: p, value: p, checked: platforms.includes(p),
312
202
  })),
313
203
  });
314
- // ESC 取消
315
204
  if (selectedPlatforms === PROMPT_CANCELLED) {
316
205
  info('已取消');
317
- return;
206
+ return {
207
+ linkedDeps: [], nestedLinkedDeps: [], generalLibs, downloadedLibs,
208
+ savedBytes: 0, finalLinkPlatforms, stats,
209
+ };
318
210
  }
319
211
  finalLinkPlatforms = selectedPlatforms;
320
212
  if (finalLinkPlatforms.length === 0) {
@@ -324,27 +216,32 @@ export async function linkProject(projectPath, options) {
324
216
  blank();
325
217
  }
326
218
  }
327
- // 预扫描所有需要下载的项,统一询问用户
328
- const downloadConfirmedLibs = new Set(); // 用户确认下载的库
329
- let skipAllDownloads = false; // 用户选择跳过所有下载
330
- if (options.download && !options.yes) {
219
+ // 6. 预扫描下载确认
220
+ const downloadConfirmedLibs = new Set();
221
+ let skipAllDownloads = false;
222
+ if (download && !yes) {
331
223
  const needDownloadItems = [];
332
224
  for (const item of classified) {
333
225
  const { dependency, status } = item;
334
226
  if (status === DependencyStatus.MISSING) {
335
227
  needDownloadItems.push({
336
- libName: dependency.libName,
337
- commit: dependency.commit,
338
- reason: '缺失',
228
+ libName: dependency.libName, commit: dependency.commit, reason: '缺失',
339
229
  });
340
230
  }
231
+ else if (status === DependencyStatus.ABSORB) {
232
+ const { missing } = await store.checkPlatformCompleteness(dependency.libName, dependency.commit, platforms);
233
+ if (missing.length > 0) {
234
+ needDownloadItems.push({
235
+ libName: dependency.libName, commit: dependency.commit,
236
+ reason: `补充平台 [${missing.join(', ')}]`,
237
+ });
238
+ }
239
+ }
341
240
  else if (status === DependencyStatus.LINK_NEW) {
342
- // 检查是否有缺失平台
343
241
  const { missing } = await store.checkPlatformCompleteness(dependency.libName, dependency.commit, platforms);
344
242
  if (missing.length > 0) {
345
243
  needDownloadItems.push({
346
- libName: dependency.libName,
347
- commit: dependency.commit,
244
+ libName: dependency.libName, commit: dependency.commit,
348
245
  reason: `补充平台 [${missing.join(', ')}]`,
349
246
  });
350
247
  }
@@ -363,7 +260,6 @@ export async function linkProject(projectPath, options) {
363
260
  skipAllDownloads = true;
364
261
  }
365
262
  else {
366
- // 用户确认下载,记录所有需要下载的库
367
263
  for (const item of needDownloadItems) {
368
264
  downloadConfirmedLibs.add(`${item.libName}@${item.commit}`);
369
265
  }
@@ -371,34 +267,28 @@ export async function linkProject(projectPath, options) {
371
267
  blank();
372
268
  }
373
269
  }
374
- else if (options.download && options.yes) {
375
- // --yes 模式:标记所有库为已确认
270
+ else if (download && yes) {
376
271
  for (const item of classified) {
377
272
  downloadConfirmedLibs.add(`${item.dependency.libName}@${item.dependency.commit}`);
378
273
  }
379
274
  }
275
+ // 7. 逐个处理依赖(LINKED/RELINK/REPLACE/ABSORB/LINK_NEW)
380
276
  try {
381
277
  for (const item of classified) {
382
278
  const { dependency, status, localPath } = item;
383
279
  const libKey = registry.getLibraryKey(dependency.libName, dependency.commit);
384
- // 检查 Store 版本兼容性(v0.5 旧结构会报错)
385
280
  await store.ensureCompatibleStore(storePath, dependency.libName, dependency.commit);
386
281
  switch (status) {
387
282
  case DependencyStatus.LINKED: {
388
- // 已链接,检查是否需要补充缺失平台
389
283
  const isLinkedGeneral = await store.isGeneralLib(dependency.libName, dependency.commit);
390
284
  if (!isLinkedGeneral) {
391
- // 平台库:检查并补充缺失平台
392
285
  const supplementResult = await supplementMissingPlatforms(dependency, platforms, registry, tx, { vars: configVars });
393
- // 注册嵌套依赖
394
286
  await registerNestedLibraries(supplementResult.nestedLibraries, projectHash);
395
287
  if (supplementResult.downloaded.length > 0) {
396
- // 有新平台下载,需要重新链接所有平台
397
288
  const linkedCommitPath = path.join(storePath, dependency.libName, dependency.commit);
398
289
  const { existing: allExisting } = await store.checkPlatformCompleteness(dependency.libName, dependency.commit, platforms);
399
290
  tx.recordOp('link', localPath, linkedCommitPath);
400
291
  await linker.linkLib(localPath, linkedCommitPath, allExisting);
401
- // 更新 StoreEntry 引用
402
292
  for (const platform of allExisting) {
403
293
  const storeKey = registry.getStoreKey(dependency.libName, dependency.commit, platform);
404
294
  registry.addStoreReference(storeKey, projectHash);
@@ -409,29 +299,21 @@ export async function linkProject(projectPath, options) {
409
299
  break;
410
300
  }
411
301
  case DependencyStatus.RELINK: {
412
- // 重建链接(Store 已有)
413
302
  const relinkCommitPath = path.join(storePath, dependency.libName, dependency.commit);
414
- // 检查是否为 General 库
415
303
  const isRelinkGeneral = await store.isGeneralLib(dependency.libName, dependency.commit);
416
304
  if (isRelinkGeneral) {
417
305
  tx.recordOp('unlink', localPath);
418
306
  await linker.unlink(localPath);
419
- // General 库:整目录链接到 _shared
420
307
  const sharedPath = path.join(relinkCommitPath, '_shared');
421
308
  tx.recordOp('link', localPath, sharedPath);
422
309
  await linker.linkGeneral(localPath, sharedPath);
423
- // 记录为 General 库
424
310
  generalLibs.add(dependency.libName);
425
311
  success(`${dependency.libName} (${dependency.commit.slice(0, 7)}) - General 库,重建链接`);
426
312
  }
427
313
  else {
428
- // 平台库:先补充缺失平台
429
314
  const relinkSupplementResult = await supplementMissingPlatforms(dependency, platforms, registry, tx, { vars: configVars });
430
- // 注册嵌套依赖
431
315
  await registerNestedLibraries(relinkSupplementResult.nestedLibraries, projectHash);
432
- // 获取所有可用平台(原有 + 新下载)
433
316
  const { existing: relinkExisting } = await store.checkPlatformCompleteness(dependency.libName, dependency.commit, platforms);
434
- // 检查:没有可用平台时警告并跳过(保持原链接状态)
435
317
  if (relinkExisting.length === 0) {
436
318
  const { KNOWN_PLATFORM_VALUES } = await import('../core/platform.js');
437
319
  const relinkCommitEntries = await fs.readdir(relinkCommitPath, { withFileTypes: true });
@@ -439,15 +321,13 @@ export async function linkProject(projectPath, options) {
439
321
  .filter(e => e.isDirectory() && e.name !== '_shared' && KNOWN_PLATFORM_VALUES.includes(e.name))
440
322
  .map(e => e.name);
441
323
  warn(`${dependency.libName} (${dependency.commit.slice(0, 7)}) - 不支持 ${platforms.join('/')} 平台 [可用: ${relinkAvailablePlatforms.join(', ')}]`);
442
- // 记录到 unavailablePlatforms
443
324
  const relinkLibKey = registry.getLibraryKey(dependency.libName, dependency.commit);
444
325
  const relinkLib = registry.getLibrary(relinkLibKey);
445
326
  if (relinkLib) {
446
327
  const unavailable = relinkLib.unavailablePlatforms || [];
447
328
  for (const p of platforms) {
448
- if (!unavailable.includes(p) && !relinkAvailablePlatforms.includes(p)) {
329
+ if (!unavailable.includes(p) && !relinkAvailablePlatforms.includes(p))
449
330
  unavailable.push(p);
450
- }
451
331
  }
452
332
  registry.updateLibrary(relinkLibKey, { unavailablePlatforms: unavailable });
453
333
  }
@@ -457,7 +337,6 @@ export async function linkProject(projectPath, options) {
457
337
  await linker.unlink(localPath);
458
338
  tx.recordOp('link', localPath, relinkCommitPath);
459
339
  await linker.linkLib(localPath, relinkCommitPath, relinkExisting);
460
- // 更新 StoreEntry 引用
461
340
  for (const platform of relinkExisting) {
462
341
  const storeKey = registry.getStoreKey(dependency.libName, dependency.commit, platform);
463
342
  registry.addStoreReference(storeKey, projectHash);
@@ -472,29 +351,21 @@ export async function linkProject(projectPath, options) {
472
351
  break;
473
352
  }
474
353
  case DependencyStatus.REPLACE: {
475
- // Store 已有,删除本地目录,直接链接
476
354
  const replaceSize = await getDirSize(localPath);
477
355
  const replaceCommitPath = path.join(storePath, dependency.libName, dependency.commit);
478
- // 检查是否为 General 库
479
356
  const isReplaceGeneral = await store.isGeneralLib(dependency.libName, dependency.commit);
480
357
  if (isReplaceGeneral) {
481
- // General 库:整目录链接到 _shared
482
358
  const sharedPath = path.join(replaceCommitPath, '_shared');
483
359
  tx.recordOp('replace', localPath, sharedPath);
484
360
  await linker.linkGeneral(localPath, sharedPath);
485
361
  savedBytes += replaceSize;
486
- // 记录为 General 库
487
362
  generalLibs.add(dependency.libName);
488
363
  success(`${dependency.libName} (${dependency.commit.slice(0, 7)}) - General 库,创建链接`);
489
364
  }
490
365
  else {
491
- // 平台库:先补充缺失平台
492
366
  const replaceSupplementResult = await supplementMissingPlatforms(dependency, platforms, registry, tx, { vars: configVars });
493
- // 注册嵌套依赖
494
367
  await registerNestedLibraries(replaceSupplementResult.nestedLibraries, projectHash);
495
- // 获取所有可用平台(原有 + 新下载)
496
368
  const { existing: replaceExisting } = await store.checkPlatformCompleteness(dependency.libName, dependency.commit, platforms);
497
- // 检查:没有可用平台时警告并跳过
498
369
  if (replaceExisting.length === 0) {
499
370
  const { KNOWN_PLATFORM_VALUES } = await import('../core/platform.js');
500
371
  const replaceCommitEntries = await fs.readdir(replaceCommitPath, { withFileTypes: true });
@@ -502,15 +373,13 @@ export async function linkProject(projectPath, options) {
502
373
  .filter(e => e.isDirectory() && e.name !== '_shared' && KNOWN_PLATFORM_VALUES.includes(e.name))
503
374
  .map(e => e.name);
504
375
  warn(`${dependency.libName} (${dependency.commit.slice(0, 7)}) - 不支持 ${platforms.join('/')} 平台 [可用: ${replaceAvailablePlatforms.join(', ')}]`);
505
- // 记录到 unavailablePlatforms
506
376
  const replaceLibKey = registry.getLibraryKey(dependency.libName, dependency.commit);
507
377
  const replaceLib = registry.getLibrary(replaceLibKey);
508
378
  if (replaceLib) {
509
379
  const unavailable = replaceLib.unavailablePlatforms || [];
510
380
  for (const p of platforms) {
511
- if (!unavailable.includes(p) && !replaceAvailablePlatforms.includes(p)) {
381
+ if (!unavailable.includes(p) && !replaceAvailablePlatforms.includes(p))
512
382
  unavailable.push(p);
513
- }
514
383
  }
515
384
  registry.updateLibrary(replaceLibKey, { unavailablePlatforms: unavailable });
516
385
  }
@@ -519,7 +388,6 @@ export async function linkProject(projectPath, options) {
519
388
  tx.recordOp('replace', localPath, replaceCommitPath);
520
389
  await linker.linkLib(localPath, replaceCommitPath, replaceExisting);
521
390
  savedBytes += replaceSize;
522
- // 更新 StoreEntry 引用
523
391
  for (const platform of replaceExisting) {
524
392
  const storeKey = registry.getStoreKey(dependency.libName, dependency.commit, platform);
525
393
  registry.addStoreReference(storeKey, projectHash);
@@ -534,44 +402,33 @@ export async function linkProject(projectPath, options) {
534
402
  break;
535
403
  }
536
404
  case DependencyStatus.ABSORB: {
537
- // 移入 Store(吸收本地目录所有平台内容)
538
405
  const storeCommitPath = path.join(storePath, dependency.libName, dependency.commit);
539
- // 1. 扫描本地平台目录
540
406
  const { KNOWN_PLATFORM_VALUES } = await import('../core/platform.js');
541
407
  const localDirEntries = await fs.readdir(localPath, { withFileTypes: true });
542
408
  const localPlatforms = localDirEntries
543
409
  .filter(entry => entry.isDirectory() && KNOWN_PLATFORM_VALUES.includes(entry.name))
544
410
  .map(entry => entry.name);
545
- // 2. 确定最终要吸收的平台(取本地存在的 ∩ 用户选择的)
546
411
  const finalPlatforms = localPlatforms.filter(p => finalLinkPlatforms.includes(p));
547
- // 2.5. 检查:本地有平台目录但没有用户请求的平台 → 警告并跳过
548
412
  if (finalPlatforms.length === 0 && localPlatforms.length > 0) {
549
413
  warn(`${dependency.libName} (${dependency.commit.slice(0, 7)}) - 不支持 ${platforms.join('/')} 平台 [本地有: ${localPlatforms.join(', ')}]`);
550
- // 记录到 unavailablePlatforms
551
414
  const absorbLibKey = registry.getLibraryKey(dependency.libName, dependency.commit);
552
415
  const absorbLib = registry.getLibrary(absorbLibKey);
553
416
  if (absorbLib) {
554
417
  const unavailable = absorbLib.unavailablePlatforms || [];
555
418
  for (const p of platforms) {
556
- if (!unavailable.includes(p) && !localPlatforms.includes(p)) {
419
+ if (!unavailable.includes(p) && !localPlatforms.includes(p))
557
420
  unavailable.push(p);
558
- }
559
421
  }
560
422
  registry.updateLibrary(absorbLibKey, { unavailablePlatforms: unavailable });
561
423
  }
562
424
  break;
563
425
  }
564
- // 3. 计算大小并显示进度(只计算一次,用于进度条和 registry)
565
426
  info(`${dependency.libName} (${dependency.commit.slice(0, 7)}) - 正在分析...`);
566
427
  const absorbSize = await getDirSize(localPath);
567
- // 4. 创建进度追踪器(用于跨文件系统复制时显示进度)
568
428
  const progressTracker = new ProgressTracker({
569
- name: ` 移入 Store`,
570
- total: absorbSize,
571
- showSpeed: true,
429
+ name: ` 移入 Store`, total: absorbSize, showSpeed: true,
572
430
  });
573
431
  tx.recordOp('absorb', storeCommitPath, localPath);
574
- // 进度回调 - 只在跨文件系统时触发
575
432
  let progressStarted = false;
576
433
  const absorbResult = await store.absorbLib(localPath, finalPlatforms, dependency.libName, dependency.commit, {
577
434
  totalSize: absorbSize,
@@ -583,62 +440,38 @@ export async function linkProject(projectPath, options) {
583
440
  progressTracker.update(copied);
584
441
  },
585
442
  });
586
- // 如果进度条启动了,停止它
587
- if (progressStarted) {
443
+ if (progressStarted)
588
444
  progressTracker.stop();
589
- }
590
- // 获取所有可链接的平台(新吸收 + 已存在跳过的)
591
445
  let absorbLinkPlatforms = [...Object.keys(absorbResult.platformPaths), ...absorbResult.skippedPlatforms];
592
- // 兼容旧结构:先添加 LibraryInfo(供 supplementMissingPlatforms 记录 unavailablePlatforms)
593
446
  const absorbLibKey = registry.getLibraryKey(dependency.libName, dependency.commit);
594
447
  if (!registry.getLibrary(absorbLibKey)) {
595
448
  registry.addLibrary({
596
- libName: dependency.libName,
597
- commit: dependency.commit,
598
- branch: dependency.branch,
599
- url: dependency.url,
600
- platforms: absorbLinkPlatforms,
601
- size: absorbSize,
602
- referencedBy: [],
603
- createdAt: new Date().toISOString(),
604
- lastAccess: new Date().toISOString(),
449
+ libName: dependency.libName, commit: dependency.commit, branch: dependency.branch,
450
+ url: dependency.url, platforms: absorbLinkPlatforms, size: absorbSize,
451
+ referencedBy: [], createdAt: new Date().toISOString(), lastAccess: new Date().toISOString(),
605
452
  });
606
453
  }
607
454
  if (absorbLinkPlatforms.length > 0) {
608
- // 为每个平台创建 StoreEntry
609
455
  for (const platform of absorbLinkPlatforms) {
610
456
  const storeKey = registry.getStoreKey(dependency.libName, dependency.commit, platform);
611
457
  if (!registry.getStore(storeKey)) {
612
458
  const integrity = await store.captureIntegrity(dependency.libName, dependency.commit, platform);
613
459
  registry.addStore({
614
- libName: dependency.libName,
615
- commit: dependency.commit,
616
- platform,
617
- branch: dependency.branch,
618
- url: dependency.url,
619
- ...integrity,
620
- usedBy: [],
621
- createdAt: new Date().toISOString(),
622
- lastAccess: new Date().toISOString(),
460
+ libName: dependency.libName, commit: dependency.commit, platform,
461
+ branch: dependency.branch, url: dependency.url, ...integrity,
462
+ usedBy: [], createdAt: new Date().toISOString(), lastAccess: new Date().toISOString(),
623
463
  });
624
464
  }
625
465
  }
626
- // 注册 _shared 目录(如果存在)
627
466
  await registerSharedStore(dependency.libName, dependency.commit, dependency.branch, dependency.url);
628
- // 注册嵌套依赖
629
467
  await registerNestedLibraries(absorbResult.nestedLibraries, projectHash);
630
- // 补充缺失平台(本地没有但用户需要的)
631
468
  const absorbSupplementResult = await supplementMissingPlatforms(dependency, platforms, registry, tx, { vars: configVars });
632
- // 注册嵌套依赖
633
469
  await registerNestedLibraries(absorbSupplementResult.nestedLibraries, projectHash);
634
- // 合并所有可链接的平台
635
470
  if (absorbSupplementResult.downloaded.length > 0) {
636
471
  absorbLinkPlatforms = [...absorbLinkPlatforms, ...absorbSupplementResult.downloaded];
637
472
  }
638
- // 创建链接
639
473
  tx.recordOp('link', localPath, storeCommitPath);
640
474
  await linker.linkLib(localPath, storeCommitPath, absorbLinkPlatforms);
641
- // 添加 StoreEntry 引用
642
475
  for (const platform of absorbLinkPlatforms) {
643
476
  const storeKey = registry.getStoreKey(dependency.libName, dependency.commit, platform);
644
477
  registry.addStoreReference(storeKey, projectHash);
@@ -651,222 +484,159 @@ export async function linkProject(projectPath, options) {
651
484
  }
652
485
  }
653
486
  else {
654
- // 检测是否为 General 类型
655
487
  const sharedPath = path.join(storeCommitPath, '_shared');
656
488
  try {
657
489
  await fs.access(sharedPath);
658
- // 检查 _shared 目录是否有内容(防止空目录静默成功)
659
490
  const sharedEntries = await fs.readdir(sharedPath);
660
491
  if (sharedEntries.length === 0) {
661
492
  warn(`${dependency.libName} (${dependency.commit.slice(0, 7)}) - _shared 目录为空,请重新下载源文件后再 link`);
662
493
  break;
663
494
  }
664
- // General 类型:整目录链接
665
495
  const { GENERAL_PLATFORM } = await import('../core/platform.js');
666
496
  tx.recordOp('link', localPath, sharedPath);
667
497
  await linker.linkGeneral(localPath, sharedPath);
668
- // Registry: StoreEntry 记录
669
498
  const storeKey = registry.getStoreKey(dependency.libName, dependency.commit, GENERAL_PLATFORM);
670
499
  if (!registry.getStore(storeKey)) {
671
500
  const integrity = await store.captureIntegrity(dependency.libName, dependency.commit, GENERAL_PLATFORM);
672
501
  registry.addStore({
673
- libName: dependency.libName,
674
- commit: dependency.commit,
675
- platform: GENERAL_PLATFORM,
676
- branch: dependency.branch,
677
- url: dependency.url,
678
- ...integrity,
679
- usedBy: [],
680
- createdAt: new Date().toISOString(),
681
- lastAccess: new Date().toISOString(),
502
+ libName: dependency.libName, commit: dependency.commit, platform: GENERAL_PLATFORM,
503
+ branch: dependency.branch, url: dependency.url, ...integrity,
504
+ usedBy: [], createdAt: new Date().toISOString(), lastAccess: new Date().toISOString(),
682
505
  });
683
506
  }
684
507
  registry.addStoreReference(storeKey, projectHash);
685
- // Registry: LibraryInfo 兼容记录
686
- const libKey = registry.getLibraryKey(dependency.libName, dependency.commit);
687
- if (!registry.getLibrary(libKey)) {
508
+ const libKeyGen = registry.getLibraryKey(dependency.libName, dependency.commit);
509
+ if (!registry.getLibrary(libKeyGen)) {
688
510
  const sharedSize = await getDirSize(sharedPath);
689
511
  registry.addLibrary({
690
- libName: dependency.libName,
691
- commit: dependency.commit,
692
- branch: dependency.branch,
693
- url: dependency.url,
694
- platforms: [GENERAL_PLATFORM],
695
- size: sharedSize,
696
- referencedBy: [],
697
- createdAt: new Date().toISOString(),
698
- lastAccess: new Date().toISOString(),
512
+ libName: dependency.libName, commit: dependency.commit, branch: dependency.branch,
513
+ url: dependency.url, platforms: [GENERAL_PLATFORM], size: sharedSize,
514
+ referencedBy: [], createdAt: new Date().toISOString(), lastAccess: new Date().toISOString(),
699
515
  });
700
516
  }
701
- // 记录为 General 库
702
517
  generalLibs.add(dependency.libName);
703
518
  hint(`${dependency.libName} (${dependency.commit.slice(0, 7)}) - General 库,整目录链接`);
704
519
  }
705
520
  catch {
706
- // _shared 也不存在
707
521
  warn(`${dependency.libName} (${dependency.commit.slice(0, 7)}) - 本地目录不含任何内容,跳过`);
708
522
  }
709
523
  }
710
524
  break;
711
525
  }
712
526
  case DependencyStatus.MISSING:
713
- // 跳过,后续并行处理
714
527
  break;
715
528
  case DependencyStatus.LINK_NEW: {
716
- // Store 已有(至少一个平台),本地无,检查平台完整性并补充缺失平台
717
529
  const linkNewCommitPath = path.join(storePath, dependency.libName, dependency.commit);
718
- // 1. 检查平台完整性
719
530
  const { missing } = await store.checkPlatformCompleteness(dependency.libName, dependency.commit, platforms);
720
- // 2. 如果有缺失平台,检查是否已确认下载
721
531
  const linkNewLibId = `${dependency.libName}@${dependency.commit}`;
722
532
  if (missing.length > 0 && !skipAllDownloads && downloadConfirmedLibs.has(linkNewLibId)) {
723
533
  info(`${dependency.libName} 缺少平台 [${missing.join(', ')}],开始下载...`);
724
- // 查找历史记录中的大小估算
725
534
  const linkNewLibKey = registry.getLibraryKey(dependency.libName, dependency.commit);
726
535
  const linkNewHistoryLib = registry.getLibrary(linkNewLibKey);
727
- // 创建下载进度监控器
728
536
  const linkNewMonitor = new DownloadMonitor({
729
- name: ` ${dependency.libName}`,
730
- estimatedSize: linkNewHistoryLib?.size,
731
- getDirSize,
537
+ name: ` ${dependency.libName}`, estimatedSize: linkNewHistoryLib?.size, getDirSize,
732
538
  });
733
- const downloadResult = await codepac.downloadToTemp({
734
- url: dependency.url,
735
- commit: dependency.commit,
736
- branch: dependency.branch,
737
- libName: dependency.libName,
738
- platforms: missing,
739
- sparse: dependency.sparse,
740
- vars: configVars,
741
- onTempDirCreated: (_tempDir, libDir) => {
742
- linkNewMonitor.start(libDir);
743
- },
744
- });
745
- // 停止进度监控
746
- await linkNewMonitor.stop();
747
- // 提示清理的平台(如果有)
539
+ let downloadResult;
540
+ try {
541
+ downloadResult = await codepac.downloadToTemp({
542
+ url: dependency.url, commit: dependency.commit, branch: dependency.branch,
543
+ libName: dependency.libName, platforms: missing, sparse: dependency.sparse, vars: configVars,
544
+ onTempDirCreated: (_tempDir, libDir) => { linkNewMonitor.start(libDir); },
545
+ onHeartbeat: (message) => { linkNewMonitor.heartbeat(message); },
546
+ });
547
+ }
548
+ finally {
549
+ await linkNewMonitor.stop();
550
+ }
748
551
  if (downloadResult.cleanedPlatforms.length > 0) {
749
552
  hint(` 已过滤: ${downloadResult.cleanedPlatforms.join(', ')}`);
750
553
  }
751
554
  try {
752
- // 过滤:只保留实际下载的且在 missing 列表中的平台
753
555
  const filteredDownloaded = downloadResult.platformDirs.filter(p => missing.includes(p));
754
556
  if (filteredDownloaded.length > 0) {
755
557
  tx.recordOp('absorb', linkNewCommitPath, downloadResult.libDir);
756
558
  const linkNewAbsorbResult = await store.absorbLib(downloadResult.libDir, filteredDownloaded, dependency.libName, dependency.commit);
757
- // 注册嵌套依赖
758
559
  await registerNestedLibraries(linkNewAbsorbResult.nestedLibraries, projectHash);
759
560
  }
760
561
  }
761
562
  finally {
762
- // 清理临时目录
763
563
  await fs.rm(downloadResult.tempDir, { recursive: true, force: true }).catch(() => { });
764
564
  }
765
565
  }
766
- // 3. 获取 Store 中实际存在的平台(下载后可能仍有缺失)
767
566
  const { existing: linkNewExisting } = await store.checkPlatformCompleteness(dependency.libName, dependency.commit, platforms);
768
- // 4. 检测是否为 General 库
769
567
  const isLinkNewGeneral = await store.isGeneralLib(dependency.libName, dependency.commit);
770
568
  if (isLinkNewGeneral) {
771
- // General 库:整目录链接
772
569
  const sharedPath = path.join(linkNewCommitPath, '_shared');
773
570
  tx.recordOp('link', localPath, sharedPath);
774
571
  await linker.linkGeneral(localPath, sharedPath);
775
- // StoreEntry 记录
776
572
  const storeKey = registry.getStoreKey(dependency.libName, dependency.commit, GENERAL_PLATFORM);
777
573
  const existingEntry = registry.getStore(storeKey);
778
574
  if (!existingEntry) {
779
575
  const integrity = await store.captureIntegrity(dependency.libName, dependency.commit, GENERAL_PLATFORM);
780
576
  registry.addStore({
781
- libName: dependency.libName,
782
- commit: dependency.commit,
783
- platform: GENERAL_PLATFORM,
784
- branch: dependency.branch,
785
- url: dependency.url,
786
- ...integrity,
787
- usedBy: [],
788
- createdAt: new Date().toISOString(),
789
- lastAccess: new Date().toISOString(),
577
+ libName: dependency.libName, commit: dependency.commit, platform: GENERAL_PLATFORM,
578
+ branch: dependency.branch, url: dependency.url, ...integrity,
579
+ usedBy: [], createdAt: new Date().toISOString(), lastAccess: new Date().toISOString(),
790
580
  });
791
581
  }
792
582
  else if (existingEntry.fileCount == null) {
793
- // 旧数据回填:升级后首次 link 时顺便记录完整性数据
794
583
  const integrity = await store.captureIntegrity(dependency.libName, dependency.commit, GENERAL_PLATFORM);
795
584
  registry.updateStore(storeKey, integrity);
796
585
  }
797
586
  registry.addStoreReference(storeKey, projectHash);
798
- // 记录为 General 库
799
587
  generalLibs.add(dependency.libName);
800
588
  success(`${dependency.libName} (${dependency.commit.slice(0, 7)}) - General 库,创建链接`);
801
589
  }
802
590
  else if (linkNewExisting.length === 0) {
803
- // 非 General 库但没有请求的平台可用 - 警告并跳过
804
- // 获取 Store 中该库的所有可用平台
805
591
  const { KNOWN_PLATFORM_VALUES } = await import('../core/platform.js');
806
592
  const commitEntries = await fs.readdir(linkNewCommitPath, { withFileTypes: true });
807
593
  const availablePlatforms = commitEntries
808
594
  .filter(e => e.isDirectory() && e.name !== '_shared' && KNOWN_PLATFORM_VALUES.includes(e.name))
809
595
  .map(e => e.name);
810
596
  warn(`${dependency.libName} (${dependency.commit.slice(0, 7)}) - 不支持 ${platforms.join('/')} 平台 [可用: ${availablePlatforms.join(', ')}]`);
811
- // 记录到 unavailablePlatforms
812
- const libKey = registry.getLibraryKey(dependency.libName, dependency.commit);
813
- const lib = registry.getLibrary(libKey);
814
- if (lib) {
815
- const unavailable = lib.unavailablePlatforms || [];
597
+ const lnLibKey = registry.getLibraryKey(dependency.libName, dependency.commit);
598
+ const lnLib = registry.getLibrary(lnLibKey);
599
+ if (lnLib) {
600
+ const unavailable = lnLib.unavailablePlatforms || [];
816
601
  for (const p of platforms) {
817
- if (!unavailable.includes(p) && !availablePlatforms.includes(p)) {
602
+ if (!unavailable.includes(p) && !availablePlatforms.includes(p))
818
603
  unavailable.push(p);
819
- }
820
604
  }
821
- registry.updateLibrary(libKey, { unavailablePlatforms: unavailable });
605
+ registry.updateLibrary(lnLibKey, { unavailablePlatforms: unavailable });
822
606
  }
823
607
  break;
824
608
  }
825
609
  else {
826
- // 普通库:linkLib 实际存在的平台
827
610
  tx.recordOp('link', localPath, linkNewCommitPath);
828
611
  await linker.linkLib(localPath, linkNewCommitPath, linkNewExisting);
829
- // 5. 为每个实际存在的平台添加 StoreReference
830
612
  for (const platform of linkNewExisting) {
831
613
  const storeKey = registry.getStoreKey(dependency.libName, dependency.commit, platform);
832
614
  const existingPlatformEntry = registry.getStore(storeKey);
833
- // 如果 StoreEntry 不存在,创建它
834
615
  if (!existingPlatformEntry) {
835
616
  const integrity = await store.captureIntegrity(dependency.libName, dependency.commit, platform);
836
617
  registry.addStore({
837
- libName: dependency.libName,
838
- commit: dependency.commit,
839
- platform,
840
- branch: dependency.branch,
841
- url: dependency.url,
842
- ...integrity,
843
- usedBy: [],
844
- createdAt: new Date().toISOString(),
845
- lastAccess: new Date().toISOString(),
618
+ libName: dependency.libName, commit: dependency.commit, platform,
619
+ branch: dependency.branch, url: dependency.url, ...integrity,
620
+ usedBy: [], createdAt: new Date().toISOString(), lastAccess: new Date().toISOString(),
846
621
  });
847
622
  }
848
623
  else if (existingPlatformEntry.fileCount == null) {
849
- // 旧数据回填:升级后首次 link 时顺便记录完整性数据
850
624
  const integrity = await store.captureIntegrity(dependency.libName, dependency.commit, platform);
851
625
  registry.updateStore(storeKey, integrity);
852
626
  }
853
627
  registry.addStoreReference(storeKey, projectHash);
854
628
  }
855
- // 注册 _shared 目录(如果存在)
856
629
  await registerSharedStore(dependency.libName, dependency.commit, dependency.branch, dependency.url);
857
630
  success(`${dependency.libName} (${dependency.commit.slice(0, 7)}) - 创建链接 [${linkNewExisting.join(', ')}]`);
858
631
  }
859
632
  break;
860
633
  }
861
634
  }
862
- // 保存事务进度
863
635
  await tx.save();
864
636
  }
865
- // 并行处理 MISSING 依赖
637
+ // 8. 并行处理 MISSING 依赖
866
638
  const missingItems = classified.filter((c) => c.status === DependencyStatus.MISSING);
867
- const downloadedLibs = [];
868
- if (missingItems.length > 0 && options.download && !skipAllDownloads) {
869
- // 根据预扫描阶段的确认结果筛选要下载的库
639
+ if (missingItems.length > 0 && download && !skipAllDownloads) {
870
640
  const toDownload = missingItems.filter((item) => {
871
641
  const libId = `${item.dependency.libName}@${item.dependency.commit}`;
872
642
  return downloadConfirmedLibs.has(libId);
@@ -874,15 +644,11 @@ export async function linkProject(projectPath, options) {
874
644
  if (toDownload.length > 0) {
875
645
  info(`开始并行下载 ${toDownload.length} 个库 (最多 ${concurrency} 个并发)...`);
876
646
  blank();
877
- // TTY 模式下使用 MultiBarManager 统一管理并行进度条
878
647
  const isTTY = process.stdout.isTTY ?? false;
879
648
  const multiBarManager = isTTY ? new MultiBarManager() : null;
880
- // 并行控制器
881
649
  const downloadLimit = pLimit(concurrency);
882
- // 为每个库下载所有选中的平台(使用 downloadToTemp + absorbLib + linkLib 新流程)
883
650
  const downloadTasks = toDownload.map((item) => downloadLimit(async () => {
884
651
  const { dependency, localPath } = item;
885
- // 并行下载日志代理:通过 multibar.log() 安全输出,避免干扰进度条
886
652
  const pLog = {
887
653
  info: (msg) => multiBarManager ? multiBarManager.log(`[info] ${msg}`) : info(msg),
888
654
  success: (msg) => multiBarManager ? multiBarManager.log(`[ok] ${msg}`) : success(msg),
@@ -892,318 +658,190 @@ export async function linkProject(projectPath, options) {
892
658
  };
893
659
  try {
894
660
  const storeCommitPath = path.join(storePath, dependency.libName, dependency.commit);
895
- // 0. 检查平台完整性:避免重复下载已存在的平台
896
661
  const { existing, missing } = await store.checkPlatformCompleteness(dependency.libName, dependency.commit, platforms);
897
- // 如果全部平台已存在,直接 linkLib,无需下载
898
662
  if (missing.length === 0) {
899
663
  pLog.info(`${dependency.libName} 所有平台已存在,直接链接...`);
900
664
  tx.recordOp('link', localPath, storeCommitPath);
901
665
  await linker.linkLib(localPath, storeCommitPath, platforms);
902
- // 为每个平台创建 StoreEntry 并添加引用
903
666
  for (const platform of platforms) {
904
667
  const storeKey = registry.getStoreKey(dependency.libName, dependency.commit, platform);
905
668
  if (!registry.getStore(storeKey)) {
906
669
  const integrity = await store.captureIntegrity(dependency.libName, dependency.commit, platform);
907
670
  registry.addStore({
908
- libName: dependency.libName,
909
- commit: dependency.commit,
910
- platform,
911
- branch: dependency.branch,
912
- url: dependency.url,
913
- ...integrity,
914
- usedBy: [],
915
- createdAt: new Date().toISOString(),
916
- lastAccess: new Date().toISOString(),
671
+ libName: dependency.libName, commit: dependency.commit, platform,
672
+ branch: dependency.branch, url: dependency.url, ...integrity,
673
+ usedBy: [], createdAt: new Date().toISOString(), lastAccess: new Date().toISOString(),
917
674
  });
918
675
  }
919
676
  registry.addStoreReference(storeKey, projectHash);
920
677
  }
921
- // 注册 _shared 目录(如果存在)
922
678
  await registerSharedStore(dependency.libName, dependency.commit, dependency.branch, dependency.url);
923
- // 兼容旧结构:也添加 LibraryInfo
924
- const libKey = registry.getLibraryKey(dependency.libName, dependency.commit);
925
- if (!registry.getLibrary(libKey)) {
679
+ const dlLibKey = registry.getLibraryKey(dependency.libName, dependency.commit);
680
+ if (!registry.getLibrary(dlLibKey)) {
926
681
  let totalSize = 0;
927
682
  for (const platform of platforms) {
928
683
  totalSize += await store.getSize(dependency.libName, dependency.commit, platform);
929
684
  }
930
685
  registry.addLibrary({
931
- libName: dependency.libName,
932
- commit: dependency.commit,
933
- branch: dependency.branch,
934
- url: dependency.url,
935
- platforms,
936
- size: totalSize,
937
- referencedBy: [],
938
- createdAt: new Date().toISOString(),
939
- lastAccess: new Date().toISOString(),
686
+ libName: dependency.libName, commit: dependency.commit, branch: dependency.branch,
687
+ url: dependency.url, platforms, size: totalSize, referencedBy: [],
688
+ createdAt: new Date().toISOString(), lastAccess: new Date().toISOString(),
940
689
  });
941
690
  }
942
691
  pLog.success(`${dependency.libName} (${dependency.commit.slice(0, 7)}) - 链接完成 [${platforms.join(', ')}]`);
943
- return {
944
- success: true,
945
- name: dependency.libName,
946
- downloadedPlatforms: platforms,
947
- skippedPlatforms: [],
948
- };
692
+ return { success: true, name: dependency.libName, downloadedPlatforms: platforms, skippedPlatforms: [] };
949
693
  }
950
- // 查找历史记录,检查已知不可用的平台
951
- const libKey = registry.getLibraryKey(dependency.libName, dependency.commit);
952
- const historyLib = registry.getLibrary(libKey);
694
+ const dlLibKey = registry.getLibraryKey(dependency.libName, dependency.commit);
695
+ const historyLib = registry.getLibrary(dlLibKey);
953
696
  const unavailablePlatforms = historyLib?.unavailablePlatforms || [];
954
- // 过滤掉已知不可用的平台
955
- const toDownload = missing.filter(p => !unavailablePlatforms.includes(p));
697
+ const dlToDownload = missing.filter(p => !unavailablePlatforms.includes(p));
956
698
  const knownUnavailable = missing.filter(p => unavailablePlatforms.includes(p));
957
- // 如果所有缺失平台都已知不可用,跳过下载
958
- if (toDownload.length === 0) {
699
+ if (dlToDownload.length === 0) {
959
700
  if (knownUnavailable.length > 0) {
960
701
  pLog.warn(`${dependency.libName} 平台 [${knownUnavailable.join(', ')}] 不支持(远程不存在)`);
961
702
  }
962
- return {
963
- success: false,
964
- name: dependency.libName,
965
- skipped: true,
966
- skippedPlatforms: missing,
967
- unsupported: true,
968
- };
703
+ return { success: false, name: dependency.libName, skipped: true, skippedPlatforms: missing, unsupported: true };
969
704
  }
970
- // 只下载未知状态的平台
971
- pLog.info(`下载 ${dependency.libName} [${toDownload.join(', ')}]...`);
705
+ pLog.info(`下载 ${dependency.libName} [${dlToDownload.join(', ')}]...`);
972
706
  const estimatedSize = historyLib?.size;
973
- // 创建下载进度监控器
974
707
  const downloadMonitor = new DownloadMonitor({
975
- name: ` ${dependency.libName}`,
976
- estimatedSize,
977
- getDirSize,
708
+ name: ` ${dependency.libName}`, estimatedSize, getDirSize,
978
709
  manager: multiBarManager ?? undefined,
979
710
  });
980
- // 1. 调用 downloadToTemp 只下载需要的平台(排除已知不可用的)
981
- const downloadResult = await codepac.downloadToTemp({
982
- url: dependency.url,
983
- commit: dependency.commit,
984
- branch: dependency.branch,
985
- libName: dependency.libName,
986
- platforms: toDownload,
987
- sparse: dependency.sparse,
988
- vars: configVars,
989
- onTempDirCreated: (_tempDir, libDir) => {
990
- // 临时目录创建后启动进度监控
991
- downloadMonitor.start(libDir);
992
- },
993
- });
994
- // 停止进度监控
995
- await downloadMonitor.stop();
996
- // 提示清理的平台(如果有)
711
+ let downloadResult;
712
+ try {
713
+ downloadResult = await codepac.downloadToTemp({
714
+ url: dependency.url, commit: dependency.commit, branch: dependency.branch,
715
+ libName: dependency.libName, platforms: dlToDownload, sparse: dependency.sparse,
716
+ vars: configVars,
717
+ onTempDirCreated: (_tempDir, libDir) => { downloadMonitor.start(libDir); },
718
+ onHeartbeat: (message) => { downloadMonitor.heartbeat(message); },
719
+ });
720
+ }
721
+ finally {
722
+ await downloadMonitor.stop();
723
+ }
997
724
  if (downloadResult.cleanedPlatforms.length > 0) {
998
725
  pLog.hint(` 已过滤: ${downloadResult.cleanedPlatforms.join(', ')}`);
999
726
  }
1000
727
  try {
1001
- // 2. 检测是否为 General 库(没有 sparse 配置,或 sparse 只有 common,且没有平台目录)
1002
728
  const isNewGeneral = (!dependency.sparse || isSparseOnlyCommon(dependency.sparse)) && downloadResult.platformDirs.length === 0;
1003
729
  if (isNewGeneral) {
1004
- // General 库:把整个下载内容移到 _shared
1005
730
  tx.recordOp('absorb', storeCommitPath, downloadResult.libDir);
1006
731
  await store.absorbGeneral(downloadResult.libDir, dependency.libName, dependency.commit);
1007
- // 创建链接
1008
732
  const sharedPath = path.join(storeCommitPath, '_shared');
1009
733
  tx.recordOp('link', localPath, sharedPath);
1010
734
  await linker.linkGeneral(localPath, sharedPath);
1011
- // StoreEntry 记录
1012
735
  const storeKey = registry.getStoreKey(dependency.libName, dependency.commit, GENERAL_PLATFORM);
1013
736
  if (!registry.getStore(storeKey)) {
1014
737
  const integrity = await store.captureIntegrity(dependency.libName, dependency.commit, GENERAL_PLATFORM);
1015
738
  registry.addStore({
1016
- libName: dependency.libName,
1017
- commit: dependency.commit,
1018
- platform: GENERAL_PLATFORM,
1019
- branch: dependency.branch,
1020
- url: dependency.url,
1021
- ...integrity,
1022
- usedBy: [],
1023
- createdAt: new Date().toISOString(),
1024
- lastAccess: new Date().toISOString(),
739
+ libName: dependency.libName, commit: dependency.commit, platform: GENERAL_PLATFORM,
740
+ branch: dependency.branch, url: dependency.url, ...integrity,
741
+ usedBy: [], createdAt: new Date().toISOString(), lastAccess: new Date().toISOString(),
1025
742
  });
1026
743
  }
1027
744
  registry.addStoreReference(storeKey, projectHash);
1028
- // LibraryInfo 兼容记录
1029
- const libKey = registry.getLibraryKey(dependency.libName, dependency.commit);
1030
- if (!registry.getLibrary(libKey)) {
745
+ const genLibKey = registry.getLibraryKey(dependency.libName, dependency.commit);
746
+ if (!registry.getLibrary(genLibKey)) {
1031
747
  const sharedSize = await getDirSize(sharedPath);
1032
748
  registry.addLibrary({
1033
- libName: dependency.libName,
1034
- commit: dependency.commit,
1035
- branch: dependency.branch,
1036
- url: dependency.url,
1037
- platforms: [GENERAL_PLATFORM],
1038
- size: sharedSize,
1039
- referencedBy: [],
1040
- createdAt: new Date().toISOString(),
1041
- lastAccess: new Date().toISOString(),
749
+ libName: dependency.libName, commit: dependency.commit, branch: dependency.branch,
750
+ url: dependency.url, platforms: [GENERAL_PLATFORM], size: sharedSize,
751
+ referencedBy: [], createdAt: new Date().toISOString(), lastAccess: new Date().toISOString(),
1042
752
  });
1043
753
  }
1044
- // 记录为 General 库
1045
754
  generalLibs.add(dependency.libName);
1046
755
  pLog.success(`${dependency.libName} (${dependency.commit.slice(0, 7)}) - General 库,下载完成`);
1047
- return {
1048
- success: true,
1049
- name: dependency.libName,
1050
- downloadedPlatforms: [GENERAL_PLATFORM],
1051
- skippedPlatforms: [],
1052
- isGeneral: true,
1053
- };
756
+ return { success: true, name: dependency.libName, downloadedPlatforms: [GENERAL_PLATFORM], skippedPlatforms: [], isGeneral: true };
1054
757
  }
1055
- // 3. 过滤平台:只保留实际下载的且在 toDownload 列表中的平台
1056
- const filteredDownloaded = downloadResult.platformDirs.filter(p => toDownload.includes(p));
1057
- // 4. 检查并记录新发现的不可用平台
1058
- const newUnavailable = toDownload.filter(p => !filteredDownloaded.includes(p));
758
+ const filteredDownloaded = downloadResult.platformDirs.filter(p => dlToDownload.includes(p));
759
+ const newUnavailable = dlToDownload.filter(p => !filteredDownloaded.includes(p));
1059
760
  if (newUnavailable.length > 0) {
1060
- // 更新 LibraryInfo 中的 unavailablePlatforms
1061
761
  const updateLibKey = registry.getLibraryKey(dependency.libName, dependency.commit);
1062
762
  if (historyLib) {
1063
763
  const updatedUnavailable = [...new Set([...unavailablePlatforms, ...newUnavailable])];
1064
764
  registry.updateLibrary(updateLibKey, { unavailablePlatforms: updatedUnavailable });
1065
765
  }
1066
766
  else {
1067
- // 如果 LibraryInfo 不存在,先创建
1068
767
  registry.addLibrary({
1069
- libName: dependency.libName,
1070
- commit: dependency.commit,
1071
- branch: dependency.branch,
1072
- url: dependency.url,
1073
- platforms: [],
1074
- size: 0,
1075
- referencedBy: [],
768
+ libName: dependency.libName, commit: dependency.commit, branch: dependency.branch,
769
+ url: dependency.url, platforms: [], size: 0, referencedBy: [],
1076
770
  unavailablePlatforms: newUnavailable,
1077
- createdAt: new Date().toISOString(),
1078
- lastAccess: new Date().toISOString(),
771
+ createdAt: new Date().toISOString(), lastAccess: new Date().toISOString(),
1079
772
  });
1080
773
  }
1081
774
  pLog.warn(`${dependency.libName} 平台 [${newUnavailable.join(', ')}] 远程不存在,已记录`);
1082
775
  }
1083
- // 5. 调用 absorbLib 将临时目录内容移入 Store(如果有下载成功的平台)
1084
776
  if (filteredDownloaded.length > 0) {
1085
777
  tx.recordOp('absorb', storeCommitPath, downloadResult.libDir);
1086
778
  const downloadAbsorbResult = await store.absorbLib(downloadResult.libDir, filteredDownloaded, dependency.libName, dependency.commit);
1087
- // 注册嵌套依赖
1088
779
  await registerNestedLibraries(downloadAbsorbResult.nestedLibraries, projectHash);
1089
780
  }
1090
- // 5. 获取所有可链接的平台(已存在 + 新下载成功的)
1091
781
  const linkPlatforms = [...existing, ...filteredDownloaded];
1092
- // 6. 检测是否为 General 库(Store 中已有 _shared)
1093
782
  const isDownloadGeneral = await store.isGeneralLib(dependency.libName, dependency.commit);
1094
783
  if (isDownloadGeneral) {
1095
- // General 库:整目录链接
1096
784
  const sharedPath = path.join(storeCommitPath, '_shared');
1097
785
  tx.recordOp('link', localPath, sharedPath);
1098
786
  await linker.linkGeneral(localPath, sharedPath);
1099
- // StoreEntry 记录
1100
787
  const storeKey = registry.getStoreKey(dependency.libName, dependency.commit, GENERAL_PLATFORM);
1101
788
  if (!registry.getStore(storeKey)) {
1102
789
  const integrity = await store.captureIntegrity(dependency.libName, dependency.commit, GENERAL_PLATFORM);
1103
790
  registry.addStore({
1104
- libName: dependency.libName,
1105
- commit: dependency.commit,
1106
- platform: GENERAL_PLATFORM,
1107
- branch: dependency.branch,
1108
- url: dependency.url,
1109
- ...integrity,
1110
- usedBy: [],
1111
- createdAt: new Date().toISOString(),
1112
- lastAccess: new Date().toISOString(),
791
+ libName: dependency.libName, commit: dependency.commit, platform: GENERAL_PLATFORM,
792
+ branch: dependency.branch, url: dependency.url, ...integrity,
793
+ usedBy: [], createdAt: new Date().toISOString(), lastAccess: new Date().toISOString(),
1113
794
  });
1114
795
  }
1115
796
  registry.addStoreReference(storeKey, projectHash);
1116
- // LibraryInfo 兼容记录
1117
- const libKey = registry.getLibraryKey(dependency.libName, dependency.commit);
1118
- if (!registry.getLibrary(libKey)) {
797
+ const genLibKey2 = registry.getLibraryKey(dependency.libName, dependency.commit);
798
+ if (!registry.getLibrary(genLibKey2)) {
1119
799
  const sharedSize = await getDirSize(sharedPath);
1120
800
  registry.addLibrary({
1121
- libName: dependency.libName,
1122
- commit: dependency.commit,
1123
- branch: dependency.branch,
1124
- url: dependency.url,
1125
- platforms: [GENERAL_PLATFORM],
1126
- size: sharedSize,
1127
- referencedBy: [],
1128
- createdAt: new Date().toISOString(),
1129
- lastAccess: new Date().toISOString(),
801
+ libName: dependency.libName, commit: dependency.commit, branch: dependency.branch,
802
+ url: dependency.url, platforms: [GENERAL_PLATFORM], size: sharedSize,
803
+ referencedBy: [], createdAt: new Date().toISOString(), lastAccess: new Date().toISOString(),
1130
804
  });
1131
805
  }
1132
- // 记录为 General 库
1133
806
  generalLibs.add(dependency.libName);
1134
807
  pLog.success(`${dependency.libName} (${dependency.commit.slice(0, 7)}) - General 库,下载完成`);
1135
- return {
1136
- success: true,
1137
- name: dependency.libName,
1138
- downloadedPlatforms: [GENERAL_PLATFORM],
1139
- skippedPlatforms: [],
1140
- isGeneral: true,
1141
- };
808
+ return { success: true, name: dependency.libName, downloadedPlatforms: [GENERAL_PLATFORM], skippedPlatforms: [], isGeneral: true };
1142
809
  }
1143
- // 普通库:无平台可链接则跳过
1144
810
  if (linkPlatforms.length === 0) {
1145
- return {
1146
- success: false,
1147
- name: dependency.libName,
1148
- skipped: true,
1149
- skippedPlatforms: platforms,
1150
- };
811
+ return { success: false, name: dependency.libName, skipped: true, skippedPlatforms: platforms };
1151
812
  }
1152
- // 6. 调用 linkLib 创建符号链接并复制共享文件
1153
813
  tx.recordOp('link', localPath, storeCommitPath);
1154
814
  await linker.linkLib(localPath, storeCommitPath, linkPlatforms);
1155
- // 7. 为每个平台创建 StoreEntry 并添加引用
1156
815
  for (const platform of linkPlatforms) {
1157
816
  const storeKey = registry.getStoreKey(dependency.libName, dependency.commit, platform);
1158
817
  if (!registry.getStore(storeKey)) {
1159
818
  const integrity = await store.captureIntegrity(dependency.libName, dependency.commit, platform);
1160
819
  registry.addStore({
1161
- libName: dependency.libName,
1162
- commit: dependency.commit,
1163
- platform,
1164
- branch: dependency.branch,
1165
- url: dependency.url,
1166
- ...integrity,
1167
- usedBy: [],
1168
- createdAt: new Date().toISOString(),
1169
- lastAccess: new Date().toISOString(),
820
+ libName: dependency.libName, commit: dependency.commit, platform,
821
+ branch: dependency.branch, url: dependency.url, ...integrity,
822
+ usedBy: [], createdAt: new Date().toISOString(), lastAccess: new Date().toISOString(),
1170
823
  });
1171
824
  }
1172
825
  registry.addStoreReference(storeKey, projectHash);
1173
826
  }
1174
- // 注册 _shared 目录(如果存在)
1175
827
  await registerSharedStore(dependency.libName, dependency.commit, dependency.branch, dependency.url);
1176
- // 兼容旧结构:也添加 LibraryInfo
1177
- const libKey = registry.getLibraryKey(dependency.libName, dependency.commit);
1178
- if (!registry.getLibrary(libKey)) {
828
+ const finalLibKey = registry.getLibraryKey(dependency.libName, dependency.commit);
829
+ if (!registry.getLibrary(finalLibKey)) {
1179
830
  let totalSize = 0;
1180
831
  for (const platform of linkPlatforms) {
1181
832
  totalSize += await store.getSize(dependency.libName, dependency.commit, platform);
1182
833
  }
1183
834
  registry.addLibrary({
1184
- libName: dependency.libName,
1185
- commit: dependency.commit,
1186
- branch: dependency.branch,
1187
- url: dependency.url,
1188
- platforms: linkPlatforms,
1189
- size: totalSize,
1190
- referencedBy: [],
1191
- createdAt: new Date().toISOString(),
1192
- lastAccess: new Date().toISOString(),
835
+ libName: dependency.libName, commit: dependency.commit, branch: dependency.branch,
836
+ url: dependency.url, platforms: linkPlatforms, size: totalSize,
837
+ referencedBy: [], createdAt: new Date().toISOString(), lastAccess: new Date().toISOString(),
1193
838
  });
1194
839
  }
1195
- // 计算未能链接的平台(用户请求但未下载也未在 Store 中的)
1196
840
  const notLinkedPlatforms = platforms.filter((p) => !linkPlatforms.includes(p));
1197
841
  pLog.success(`${dependency.libName} (${dependency.commit.slice(0, 7)}) - 下载完成 [${linkPlatforms.join(', ')}]`);
1198
- return {
1199
- success: true,
1200
- name: dependency.libName,
1201
- downloadedPlatforms: linkPlatforms,
1202
- skippedPlatforms: notLinkedPlatforms,
1203
- };
842
+ return { success: true, name: dependency.libName, downloadedPlatforms: linkPlatforms, skippedPlatforms: notLinkedPlatforms };
1204
843
  }
1205
844
  finally {
1206
- // 清理临时目录(无论成功还是失败)
1207
845
  await fs.rm(downloadResult.tempDir, { recursive: true, force: true }).catch(() => { });
1208
846
  }
1209
847
  }
@@ -1213,16 +851,13 @@ export async function linkProject(projectPath, options) {
1213
851
  }
1214
852
  }));
1215
853
  const results = await Promise.all(downloadTasks);
1216
- // 停止多进度条管理器
1217
854
  multiBarManager?.stop();
1218
855
  const succeeded = results.filter((r) => r.success);
1219
856
  const failed = results.filter((r) => !r.success && !('skipped' in r && r.skipped));
1220
857
  blank();
1221
858
  info(`下载完成: ${succeeded.length}/${toDownload.length} 个库`);
1222
- if (failed.length > 0) {
859
+ if (failed.length > 0)
1223
860
  warn(`${failed.length} 个库下载失败`);
1224
- }
1225
- // 汇总提示跳过的平台
1226
861
  const allSkipped = [];
1227
862
  for (const r of results) {
1228
863
  if ('skippedPlatforms' in r && r.skippedPlatforms && r.skippedPlatforms.length > 0) {
@@ -1236,116 +871,365 @@ export async function linkProject(projectPath, options) {
1236
871
  warn(` - ${item.name} / ${item.platforms.join(', ')}`);
1237
872
  }
1238
873
  }
1239
- // 记录成功下载的库
1240
- for (const r of succeeded) {
874
+ for (const r of succeeded)
1241
875
  downloadedLibs.push(r.name);
1242
- }
1243
876
  }
1244
877
  }
1245
- else if (missingItems.length > 0 && !options.download) {
878
+ else if (missingItems.length > 0 && !download) {
1246
879
  for (const item of missingItems) {
1247
880
  warn(`${item.dependency.libName} (${item.dependency.commit.slice(0, 7)}) - 缺失 (跳过下载)`);
1248
881
  }
1249
882
  }
1250
- // ============ 处理嵌套依赖 (actions) ============
883
+ // 9. 处理嵌套依赖 (actions)
1251
884
  const topLevelConfig = await parseCodepacDep(configPath);
1252
885
  const actions = extractActions(topLevelConfig);
1253
- // 嵌套依赖记录(用于 registry)
1254
886
  const nestedLinkedDeps = [];
1255
887
  if (actions.length > 0) {
1256
888
  blank();
1257
889
  separator();
1258
890
  info(`发现 ${actions.length} 个嵌套依赖配置`);
1259
891
  const nestedContext = {
1260
- depth: 0,
1261
- processedConfigs: new Set([configPath]),
1262
- platforms,
1263
- vars: configVars,
892
+ depth: 0, processedConfigs: new Set([configPath]), platforms, vars: configVars,
1264
893
  };
1265
894
  const thirdPartyDir = path.dirname(configPath);
1266
- // 依次处理每个 action
1267
895
  for (const action of actions) {
1268
896
  await processAction(action, nestedContext, thirdPartyDir, {
1269
- tx,
1270
- registry,
1271
- projectHash,
1272
- projectRoot: finalPath,
1273
- dryRun: options.dryRun,
1274
- download: options.download,
1275
- yes: options.yes,
1276
- generalLibs,
1277
- downloadedLibs,
1278
- nestedLinkedDeps,
897
+ tx, registry, projectHash, projectRoot, dryRun, download, yes,
898
+ generalLibs, downloadedLibs, nestedLinkedDeps,
1279
899
  });
1280
900
  }
1281
901
  }
1282
- // 获取旧引用(用于后续引用关系更新)
1283
- const oldStoreKeys = registry.getProjectStoreKeys(projectHash);
1284
- // 更新项目信息
1285
- const relConfigPath = getRelativeConfigPath(finalPath, configPath);
1286
- // 使用主平台作为依赖的 platform 字段(兼容旧结构)
902
+ // 10. 同步 cache 文件
903
+ await syncCacheFile(configPath);
904
+ // 11. 构建返回结果
1287
905
  const primaryPlatform = platforms[0];
1288
- const topLevelDeps = classified
906
+ const linkedDeps = classified
1289
907
  .filter((c) => {
1290
- if (c.status === DependencyStatus.MISSING) {
1291
- // 只包含成功下载的库
908
+ if (c.status === DependencyStatus.MISSING)
1292
909
  return downloadedLibs.includes(c.dependency.libName);
1293
- }
1294
910
  return true;
1295
911
  })
1296
912
  .map((c) => ({
1297
913
  libName: c.dependency.libName,
1298
914
  commit: c.dependency.commit,
1299
- // General 库使用 'general' 平台,普通库使用主平台
1300
915
  platform: generalLibs.has(c.dependency.libName) ? GENERAL_PLATFORM : primaryPlatform,
1301
- linkedPath: path.relative(finalPath, c.localPath),
916
+ linkedPath: path.relative(projectRoot, c.localPath),
917
+ scope,
1302
918
  }));
1303
- // 合并顶层依赖和嵌套依赖
1304
- const newDependencies = [...topLevelDeps, ...nestedLinkedDeps];
919
+ return {
920
+ linkedDeps, nestedLinkedDeps, generalLibs, downloadedLibs, savedBytes, finalLinkPlatforms, stats,
921
+ };
922
+ }
923
+ catch (err) {
924
+ // 将异常传播给调用者(linkProject 的 try-catch 会处理回滚)
925
+ throw err;
926
+ }
927
+ }
928
+ /**
929
+ * 执行链接操作
930
+ */
931
+ export async function linkProject(projectPath, options) {
932
+ const absolutePath = resolvePath(projectPath);
933
+ // === 阶段 1: 初始化 ===
934
+ const cfg = await config.load();
935
+ if (cfg?.logLevel)
936
+ setLogLevel(cfg.logLevel);
937
+ if (cfg?.proxy)
938
+ setProxyConfig(cfg.proxy);
939
+ const concurrency = cfg?.concurrency ?? 5;
940
+ const registry = getRegistry();
941
+ await registry.load();
942
+ const existingProject = registry.getProjectByPath(absolutePath);
943
+ const rememberedPlatforms = existingProject?.platforms;
944
+ // 确定平台列表
945
+ let platforms;
946
+ if (options.platform && options.platform.length > 0) {
947
+ platforms = parsePlatformArgs(options.platform);
948
+ }
949
+ else if (!options.yes && process.stdout.isTTY) {
950
+ const selectedPlatforms = await selectPlatforms(rememberedPlatforms);
951
+ if (selectedPlatforms === PROMPT_CANCELLED) {
952
+ info('已取消');
953
+ return;
954
+ }
955
+ platforms = selectedPlatforms;
956
+ if (platforms.length === 0) {
957
+ error('至少需要选择一个平台');
958
+ process.exit(EXIT_CODES.MISUSE);
959
+ }
960
+ }
961
+ else {
962
+ error('非交互模式下必须使用 -p 指定平台');
963
+ hint('示例: tanmi-dock link -p mac ios');
964
+ process.exit(EXIT_CODES.MISUSE);
965
+ }
966
+ // 检查项目路径
967
+ try {
968
+ const stat = await fs.stat(absolutePath);
969
+ if (!stat.isDirectory()) {
970
+ error(`路径不是目录: ${absolutePath}`);
971
+ process.exit(EXIT_CODES.NOINPUT);
972
+ }
973
+ }
974
+ catch {
975
+ error(`路径不存在: ${absolutePath}`);
976
+ process.exit(EXIT_CODES.NOINPUT);
977
+ }
978
+ // 发现可选配置文件
979
+ const thirdpartyDir = path.join(absolutePath, '3rdparty');
980
+ const configDiscovery = await findAllCodepacConfigs(thirdpartyDir);
981
+ let selectedOptionalConfigs = [];
982
+ if (configDiscovery && configDiscovery.optionalConfigs.length > 0) {
983
+ if (options.config && options.config.length > 0) {
984
+ for (const configName of options.config) {
985
+ const found = configDiscovery.optionalConfigs.find(c => c.name === configName);
986
+ if (!found) {
987
+ error(`找不到指定的配置: ${configName}`);
988
+ hint(`可用配置: ${configDiscovery.optionalConfigs.map(c => c.name).join(', ')}`);
989
+ process.exit(EXIT_CODES.MISUSE);
990
+ }
991
+ selectedOptionalConfigs.push(found);
992
+ }
993
+ }
994
+ else if (options.yes) {
995
+ // --yes 模式:跳过可选配置选择
996
+ }
997
+ else if (process.stdout.isTTY) {
998
+ const rememberedConfigs = existingProject?.optionalConfigs ?? [];
999
+ const selectOptions = {
1000
+ isTTY: true,
1001
+ specifiedConfigs: rememberedConfigs,
1002
+ };
1003
+ const selected = await selectOptionalConfigs(configDiscovery.optionalConfigs, selectOptions);
1004
+ if (selected === PROMPT_CANCELLED) {
1005
+ info('已取消');
1006
+ return;
1007
+ }
1008
+ selectedOptionalConfigs = selected;
1009
+ }
1010
+ else {
1011
+ error('发现可选配置文件,非交互模式下必须使用 --config 或 --yes 参数');
1012
+ hint(`可用配置: ${configDiscovery.optionalConfigs.map(c => c.name).join(', ')}`);
1013
+ hint(`示例: td link --config ${configDiscovery.optionalConfigs[0].name}`);
1014
+ hint('或使用 --yes 跳过可选配置');
1015
+ process.exit(EXIT_CODES.MISUSE);
1016
+ }
1017
+ }
1018
+ // 查找主配置文件
1019
+ const { findCodepacConfig } = await import('../core/parser.js');
1020
+ const mainConfigPath = await findCodepacConfig(absolutePath);
1021
+ if (!mainConfigPath) {
1022
+ error(`找不到 codepac-dep.json 配置文件,已搜索: 3rdparty, .`);
1023
+ process.exit(EXIT_CODES.DATAERR);
1024
+ }
1025
+ // === 阶段 2: Submodule 检测 ===
1026
+ let selectedSubmodules = [];
1027
+ if (options.submodules !== false) {
1028
+ const submoduleConfigs = await findSubmoduleConfigs(absolutePath);
1029
+ if (submoduleConfigs.length > 0) {
1030
+ selectedSubmodules = await selectSubmodules(submoduleConfigs, options, existingProject?.submodules);
1031
+ // 为选中的 submodule 处理可选配置
1032
+ for (const sub of selectedSubmodules) {
1033
+ if (sub.optionalConfigs.length > 0 && !options.yes && process.stdout.isTTY) {
1034
+ info(`${sub.name} 发现可选配置:`);
1035
+ const selected = await selectOptionalConfigs(sub.optionalConfigs, { isTTY: true, specifiedConfigs: [] });
1036
+ if (selected !== PROMPT_CANCELLED) {
1037
+ sub.selectedOptionalConfigs = selected;
1038
+ }
1039
+ }
1040
+ }
1041
+ }
1042
+ }
1043
+ // === 阶段 3: 规范化 + 事务准备 ===
1044
+ const normalizedRoot = normalizeProjectRoot(absolutePath, mainConfigPath);
1045
+ const wasNormalized = normalizedRoot !== absolutePath;
1046
+ if (wasNormalized) {
1047
+ info(`项目根目录规范化: ${absolutePath} → ${normalizedRoot}`);
1048
+ const oldHash = registry.hashPath(absolutePath);
1049
+ const oldProject = registry.getProject(oldHash);
1050
+ if (oldProject) {
1051
+ info('迁移旧的项目登记...');
1052
+ registry.removeProject(oldHash);
1053
+ }
1054
+ }
1055
+ const absConfigPath = path.resolve(normalizedRoot, getRelativeConfigPath(normalizedRoot, mainConfigPath));
1056
+ const allProjects = registry.listProjects();
1057
+ for (const proj of allProjects) {
1058
+ if (pathsEqual(proj.path, normalizedRoot))
1059
+ continue;
1060
+ const projAbsConfig = path.resolve(proj.path, proj.configPath);
1061
+ if (pathsEqual(projAbsConfig, absConfigPath)) {
1062
+ info(`清理重复登记: ${proj.path}`);
1063
+ registry.removeProject(registry.hashPath(proj.path));
1064
+ }
1065
+ }
1066
+ const finalPath = normalizedRoot;
1067
+ // 检查是否有未完成的事务需要恢复
1068
+ const pendingTx = await Transaction.findPending();
1069
+ if (pendingTx) {
1070
+ warn(`发现未完成的事务 (${pendingTx.id.slice(0, 8)})`);
1071
+ info('正在尝试回滚...');
1072
+ try {
1073
+ await pendingTx.rollback();
1074
+ success('事务回滚完成');
1075
+ }
1076
+ catch (err) {
1077
+ error(`回滚失败: ${err.message}`);
1078
+ hint('请手动检查 Store 和项目目录状态');
1079
+ }
1080
+ }
1081
+ const storePath = await store.getStorePath();
1082
+ const projectHash = registry.hashPath(finalPath);
1083
+ const tx = new Transaction(`link:${finalPath}`);
1084
+ await tx.begin();
1085
+ // === 阶段 4: 依次执行 linkScope ===
1086
+ try {
1087
+ // 主项目
1088
+ const mainResult = await linkScope({
1089
+ scopeName: '主项目',
1090
+ configPath: mainConfigPath,
1091
+ platforms,
1092
+ finalLinkPlatforms: platforms,
1093
+ scanExtraPlatforms: true,
1094
+ registry, tx, projectHash,
1095
+ projectRoot: finalPath,
1096
+ storePath,
1097
+ download: options.download,
1098
+ dryRun: options.dryRun,
1099
+ yes: options.yes,
1100
+ concurrency,
1101
+ optionalConfigs: selectedOptionalConfigs,
1102
+ });
1103
+ const finalLinkPlatforms = mainResult.finalLinkPlatforms;
1104
+ // 如果 dry-run 模式,linkScope 内已显示信息
1105
+ if (options.dryRun) {
1106
+ // submodule 也需要 dry-run 显示
1107
+ for (const sub of selectedSubmodules) {
1108
+ blank();
1109
+ separator();
1110
+ info(`子模块: ${sub.name}`);
1111
+ await linkScope({
1112
+ scopeName: sub.name,
1113
+ configPath: sub.configPath,
1114
+ platforms,
1115
+ finalLinkPlatforms,
1116
+ scanExtraPlatforms: false,
1117
+ registry, tx, projectHash,
1118
+ projectRoot: finalPath,
1119
+ storePath,
1120
+ download: options.download,
1121
+ dryRun: true,
1122
+ yes: options.yes,
1123
+ concurrency,
1124
+ optionalConfigs: sub.selectedOptionalConfigs,
1125
+ scope: sub.relativePath,
1126
+ });
1127
+ }
1128
+ return;
1129
+ }
1130
+ // Submodule scopes
1131
+ const subResults = [];
1132
+ for (const sub of selectedSubmodules) {
1133
+ blank();
1134
+ separator();
1135
+ info(`链接子模块: ${sub.name}`);
1136
+ const subResult = await linkScope({
1137
+ scopeName: sub.name,
1138
+ configPath: sub.configPath,
1139
+ platforms,
1140
+ finalLinkPlatforms,
1141
+ scanExtraPlatforms: false,
1142
+ registry, tx, projectHash,
1143
+ projectRoot: finalPath,
1144
+ storePath,
1145
+ download: options.download,
1146
+ dryRun: options.dryRun,
1147
+ yes: options.yes,
1148
+ concurrency,
1149
+ optionalConfigs: sub.selectedOptionalConfigs,
1150
+ scope: sub.relativePath,
1151
+ });
1152
+ subResults.push(subResult);
1153
+ }
1154
+ // === 阶段 5: 合并结果、注册项目 ===
1155
+ const allGeneralLibs = new Set([
1156
+ ...mainResult.generalLibs,
1157
+ ...subResults.flatMap(r => [...r.generalLibs]),
1158
+ ]);
1159
+ const allDownloadedLibs = [
1160
+ ...mainResult.downloadedLibs,
1161
+ ...subResults.flatMap(r => r.downloadedLibs),
1162
+ ];
1163
+ // 合并所有依赖
1164
+ const allLinkedDeps = [
1165
+ ...mainResult.linkedDeps,
1166
+ ...mainResult.nestedLinkedDeps,
1167
+ ...subResults.flatMap(r => [...r.linkedDeps, ...r.nestedLinkedDeps]),
1168
+ ];
1169
+ // 获取旧引用
1170
+ const oldStoreKeys = registry.getProjectStoreKeys(projectHash);
1171
+ // 更新项目信息
1172
+ const relConfigPath = getRelativeConfigPath(finalPath, mainConfigPath);
1305
1173
  registry.addProject({
1306
1174
  path: finalPath,
1307
1175
  configPath: relConfigPath,
1308
1176
  lastLinked: new Date().toISOString(),
1309
- platforms: finalLinkPlatforms, // 记录实际链接的平台(包括用户选择的额外平台)
1310
- dependencies: newDependencies,
1311
- optionalConfigs: selectedOptionalConfigs.length > 0 ? selectedOptionalConfigs.map(c => c.name) : undefined,
1177
+ platforms: finalLinkPlatforms,
1178
+ dependencies: allLinkedDeps,
1179
+ optionalConfigs: selectedOptionalConfigs.length > 0
1180
+ ? selectedOptionalConfigs.map(c => c.name) : undefined,
1181
+ submodules: selectedSubmodules.length > 0
1182
+ ? selectedSubmodules.map(s => s.relativePath) : undefined,
1312
1183
  });
1313
1184
  // 更新 Store 引用关系
1314
- const newStoreKeys = newDependencies.map((d) => registry.getStoreKey(d.libName, d.commit, d.platform));
1315
- // 移除不再使用的引用(设置 unlinkedAt)
1185
+ const newStoreKeys = allLinkedDeps.map((d) => registry.getStoreKey(d.libName, d.commit, d.platform));
1316
1186
  for (const key of oldStoreKeys) {
1317
1187
  if (!newStoreKeys.includes(key)) {
1318
1188
  registry.removeStoreReference(key, projectHash);
1319
1189
  }
1320
1190
  }
1321
- // 添加新引用(清除 unlinkedAt)
1322
1191
  for (const key of newStoreKeys) {
1323
1192
  registry.addStoreReference(key, projectHash);
1324
1193
  }
1325
1194
  await registry.save();
1326
- // 事务提交成功
1327
1195
  await tx.commit();
1328
- // 同步 cache 文件(兼容 codepac 的 checkValid.js 检测)
1329
- await syncCacheFile(configPath);
1330
- // 显示统计
1196
+ // === 阶段 6: 统计报告 ===
1197
+ const allSavedBytes = mainResult.savedBytes + subResults.reduce((sum, r) => sum + r.savedBytes, 0);
1331
1198
  blank();
1332
1199
  separator();
1333
- const topLevelLinked = stats.linked +
1334
- stats.relink +
1335
- stats.replace +
1336
- stats.absorb +
1337
- stats.linkNew +
1338
- downloadedLibs.length;
1339
- const nestedLinked = nestedLinkedDeps.length;
1340
- const totalLinked = topLevelLinked + nestedLinked;
1341
- if (nestedLinked > 0) {
1342
- info(`完成: 链接 ${totalLinked} 个库 (顶层 ${topLevelLinked}, 嵌套 ${nestedLinked})`);
1200
+ if (selectedSubmodules.length > 0) {
1201
+ // 分组显示
1202
+ const mainLinked = mainResult.stats.linked + mainResult.stats.relink + mainResult.stats.replace +
1203
+ mainResult.stats.absorb + mainResult.stats.linkNew + mainResult.downloadedLibs.length;
1204
+ const mainNested = mainResult.nestedLinkedDeps.length;
1205
+ info(`主项目: ${mainLinked + mainNested} 个库` +
1206
+ (mainNested > 0 ? ` (顶层 ${mainLinked}, 嵌套 ${mainNested})` : ''));
1207
+ let totalLinked = mainLinked + mainNested;
1208
+ for (let i = 0; i < subResults.length; i++) {
1209
+ const subR = subResults[i];
1210
+ const subLinked = subR.stats.linked + subR.stats.relink + subR.stats.replace +
1211
+ subR.stats.absorb + subR.stats.linkNew + subR.downloadedLibs.length;
1212
+ const subNested = subR.nestedLinkedDeps.length;
1213
+ info(`${selectedSubmodules[i].name}: ${subLinked + subNested} 个库` +
1214
+ (subNested > 0 ? ` (顶层 ${subLinked}, 嵌套 ${subNested})` : ''));
1215
+ totalLinked += subLinked + subNested;
1216
+ }
1217
+ info(`完成: 共链接 ${totalLinked} 个库`);
1343
1218
  }
1344
1219
  else {
1345
- info(`完成: 链接 ${totalLinked} 个库`);
1220
+ const mainLinked = mainResult.stats.linked + mainResult.stats.relink + mainResult.stats.replace +
1221
+ mainResult.stats.absorb + mainResult.stats.linkNew + mainResult.downloadedLibs.length;
1222
+ const mainNested = mainResult.nestedLinkedDeps.length;
1223
+ const totalLinked = mainLinked + mainNested;
1224
+ if (mainNested > 0) {
1225
+ info(`完成: 链接 ${totalLinked} 个库 (顶层 ${mainLinked}, 嵌套 ${mainNested})`);
1226
+ }
1227
+ else {
1228
+ info(`完成: 链接 ${totalLinked} 个库`);
1229
+ }
1346
1230
  }
1347
- if (savedBytes > 0) {
1348
- info(`本次节省: ${formatSize(savedBytes)}`);
1231
+ if (allSavedBytes > 0) {
1232
+ info(`本次节省: ${formatSize(allSavedBytes)}`);
1349
1233
  }
1350
1234
  const totalSize = await store.getTotalSize();
1351
1235
  info(`Store 总计: ${formatSize(totalSize)}`);
@@ -1409,6 +1293,7 @@ export async function linkProject(projectPath, options) {
1409
1293
  catch (err) {
1410
1294
  // 链接过程出错,回滚事务
1411
1295
  error(`链接失败: ${err.message}`);
1296
+ hintStoreRepairCommand(err);
1412
1297
  warn('正在回滚事务...');
1413
1298
  try {
1414
1299
  await tx.rollback();
@@ -1421,6 +1306,17 @@ export async function linkProject(projectPath, options) {
1421
1306
  process.exit(1);
1422
1307
  }
1423
1308
  }
1309
+ function hintStoreRepairCommand(err) {
1310
+ const message = err.message;
1311
+ const isSharedPlatformConflict = message.includes('EEXIST') &&
1312
+ message.includes('_shared') &&
1313
+ KNOWN_PLATFORM_VALUES.some((platform) => message.includes(`${path.sep}_shared${path.sep}${platform}`) || message.includes(`/_shared/${platform}`));
1314
+ if (!isSharedPlatformConflict) {
1315
+ return;
1316
+ }
1317
+ hint('检测到 Store 结构冲突,可能是 _shared 中混入了平台目录。');
1318
+ hint('请运行 `td check --fix` 清理损坏的 Store 条目后再重试。');
1319
+ }
1424
1320
  /**
1425
1321
  * 注册 _shared 目录的 StoreEntry(如果存在)
1426
1322
  * 用于追踪平台库的共享内容大小
@@ -2092,6 +1988,40 @@ async function linkNestedDependencies(dependencies, params) {
2092
1988
  await registerNestedLibraries(resolveResult.absorbResult.nestedLibraries, projectHash);
2093
1989
  }
2094
1990
  storeHas = true;
1991
+ // 吸收为 General 后,检查是否实际需要平台内容
1992
+ // 本地可能只有部分文件(如只有 _shared 内容),被误分类为 General
1993
+ if (resolveResult.isGeneral && download) {
1994
+ const hasPlatformSparse = dep.sparse && !isSparseOnlyCommon(dep.sparse);
1995
+ if (hasPlatformSparse && availablePlatforms.length > 0) {
1996
+ try {
1997
+ const downloadResult = await codepac.downloadToTemp({
1998
+ url: dep.url,
1999
+ commit: dep.commit,
2000
+ branch: dep.branch,
2001
+ libName: dep.libName,
2002
+ platforms: availablePlatforms,
2003
+ sparse: dep.sparse,
2004
+ vars,
2005
+ });
2006
+ if (downloadResult.platformDirs.length > 0) {
2007
+ // 有平台内容 → 吸收平台目录,重新分类为平台库
2008
+ tx.recordOp('absorb', storeCommitPath, downloadResult.libDir);
2009
+ const nestedAbsorbResult = await store.absorbLib(downloadResult.libDir, downloadResult.platformDirs, dep.libName, dep.commit);
2010
+ await registerNestedLibraries(nestedAbsorbResult.nestedLibraries, projectHash);
2011
+ existingPlatforms.push(...downloadResult.platformDirs);
2012
+ isGeneral = false;
2013
+ info(`${indent} ${dep.libName} - 补充平台内容 [${downloadResult.platformDirs.join(', ')}]`);
2014
+ }
2015
+ if (downloadResult.cleanedPlatforms.length > 0) {
2016
+ hint(`${indent} 已过滤: ${downloadResult.cleanedPlatforms.join(', ')}`);
2017
+ }
2018
+ }
2019
+ catch {
2020
+ // 下载失败,保持 General 分类
2021
+ warn(`${indent} ${dep.libName} - 平台内容下载失败,保持 General 分类`);
2022
+ }
2023
+ }
2024
+ }
2095
2025
  }
2096
2026
  // 更新 localExists 状态(可能已被删除或 absorb)
2097
2027
  try {
@@ -2207,15 +2137,30 @@ async function linkNestedDependencies(dependencies, params) {
2207
2137
  continue;
2208
2138
  }
2209
2139
  try {
2210
- const downloadResult = await codepac.downloadToTemp({
2211
- url: dep.url,
2212
- commit: dep.commit,
2213
- branch: dep.branch,
2214
- libName: dep.libName,
2215
- platforms: availablePlatforms,
2216
- sparse: dep.sparse,
2217
- vars,
2140
+ const nestedLibKey = registry.getLibraryKey(dep.libName, dep.commit);
2141
+ const nestedHistoryLib = registry.getLibrary(nestedLibKey);
2142
+ const downloadMonitor = new DownloadMonitor({
2143
+ name: `${indent} ${dep.libName}`,
2144
+ estimatedSize: nestedHistoryLib?.size,
2145
+ getDirSize,
2218
2146
  });
2147
+ let downloadResult;
2148
+ try {
2149
+ downloadResult = await codepac.downloadToTemp({
2150
+ url: dep.url,
2151
+ commit: dep.commit,
2152
+ branch: dep.branch,
2153
+ libName: dep.libName,
2154
+ platforms: availablePlatforms,
2155
+ sparse: dep.sparse,
2156
+ vars,
2157
+ onTempDirCreated: (_tempDir, libDir) => { downloadMonitor.start(libDir); },
2158
+ onHeartbeat: (message) => { downloadMonitor.heartbeat(message); },
2159
+ });
2160
+ }
2161
+ finally {
2162
+ await downloadMonitor.stop();
2163
+ }
2219
2164
  // 提示清理的平台(如果有)
2220
2165
  if (downloadResult.cleanedPlatforms.length > 0) {
2221
2166
  hint(`${indent} 已过滤: ${downloadResult.cleanedPlatforms.join(', ')}`);