tanmi-dock 1.0.4 → 1.0.5

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 (47) hide show
  1. package/dist/commands/check.d.ts +30 -4
  2. package/dist/commands/check.d.ts.map +1 -1
  3. package/dist/commands/check.js +453 -84
  4. package/dist/commands/check.js.map +1 -1
  5. package/dist/commands/link.d.ts.map +1 -1
  6. package/dist/commands/link.js +109 -35
  7. package/dist/commands/link.js.map +1 -1
  8. package/dist/commands/status.d.ts.map +1 -1
  9. package/dist/commands/status.js +9 -3
  10. package/dist/commands/status.js.map +1 -1
  11. package/dist/commands/unlink.d.ts.map +1 -1
  12. package/dist/commands/unlink.js +9 -0
  13. package/dist/commands/unlink.js.map +1 -1
  14. package/dist/core/config.d.ts.map +1 -1
  15. package/dist/core/config.js +4 -3
  16. package/dist/core/config.js.map +1 -1
  17. package/dist/core/space/calibration-executor.d.ts +2 -1
  18. package/dist/core/space/calibration-executor.d.ts.map +1 -1
  19. package/dist/core/space/calibration-executor.js +2 -0
  20. package/dist/core/space/calibration-executor.js.map +1 -1
  21. package/dist/core/space/inventory.d.ts +2 -1
  22. package/dist/core/space/inventory.d.ts.map +1 -1
  23. package/dist/core/space/inventory.js +23 -0
  24. package/dist/core/space/inventory.js.map +1 -1
  25. package/dist/core/space/service.d.ts +2 -2
  26. package/dist/core/space/service.d.ts.map +1 -1
  27. package/dist/core/space/service.js +2 -2
  28. package/dist/core/space/service.js.map +1 -1
  29. package/dist/core/space/types.d.ts +7 -0
  30. package/dist/core/space/types.d.ts.map +1 -1
  31. package/dist/core/space/types.js.map +1 -1
  32. package/dist/core/store-write-protection.d.ts +30 -0
  33. package/dist/core/store-write-protection.d.ts.map +1 -0
  34. package/dist/core/store-write-protection.js +320 -0
  35. package/dist/core/store-write-protection.js.map +1 -0
  36. package/dist/core/transaction.d.ts +12 -3
  37. package/dist/core/transaction.d.ts.map +1 -1
  38. package/dist/core/transaction.js +160 -16
  39. package/dist/core/transaction.js.map +1 -1
  40. package/dist/utils/fs-utils.d.ts.map +1 -1
  41. package/dist/utils/fs-utils.js +46 -9
  42. package/dist/utils/fs-utils.js.map +1 -1
  43. package/dist/utils/logger.d.ts +6 -0
  44. package/dist/utils/logger.d.ts.map +1 -1
  45. package/dist/utils/logger.js +43 -3
  46. package/dist/utils/logger.js.map +1 -1
  47. package/package.json +1 -1
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import path from 'path';
5
5
  import fs from 'fs/promises';
6
+ import crypto from 'crypto';
6
7
  import { Command } from 'commander';
7
8
  import { ensureInitialized } from '../core/guard.js';
8
9
  import * as config from '../core/config.js';
@@ -26,6 +27,7 @@ import { withGlobalLock } from '../utils/global-lock.js';
26
27
  import { selectPlatforms, parsePlatformArgs, selectOption, selectOptionalConfigs, PROMPT_CANCELLED } from '../utils/prompt.js';
27
28
  import pLimit from 'p-limit';
28
29
  import { EXIT_CODES } from '../utils/exit-codes.js';
30
+ import { assertPathOutsideStore, isMaterializedStoreDirectory, prepareWritableDirectory, } from '../core/store-write-protection.js';
29
31
  /**
30
32
  * 创建 link 命令
31
33
  */
@@ -719,7 +721,7 @@ async function linkScope(params) {
719
721
  const linkedCommitPath = path.join(storePath, dependency.libName, dependency.commit);
720
722
  const { actualExisting: allExisting, satisfiedRequested } = await resolveRequestedStoreState(dependency, requestedPlatforms, configVars);
721
723
  clearSatisfiedAutoUnavailablePlatforms(registry, dependency, satisfiedRequested);
722
- tx.recordOp('link', localPath, linkedCommitPath);
724
+ await tx.recordOp('link', localPath, linkedCommitPath);
723
725
  await linker.linkLib(localPath, linkedCommitPath, allExisting);
724
726
  await ensureLinkedRegistryState(dependency.libName, dependency.commit, dependency.branch, dependency.url, allExisting, projectHash, false);
725
727
  success(`${dependency.libName} (${dependency.commit.slice(0, 7)}) - 已补充平台 [${supplementResult.downloaded.join(', ')}]`);
@@ -728,6 +730,9 @@ async function linkScope(params) {
728
730
  const { actualExisting: linkedExisting, satisfiedRequested } = await resolveRequestedStoreState(dependency, requestedPlatforms, configVars);
729
731
  clearSatisfiedAutoUnavailablePlatforms(registry, dependency, satisfiedRequested);
730
732
  await ensureLinkedRegistryState(dependency.libName, dependency.commit, dependency.branch, dependency.url, isLinkedGeneral ? [GENERAL_PLATFORM] : linkedExisting, projectHash, isLinkedGeneral);
733
+ if (isLinkedGeneral) {
734
+ generalLibs.add(dependency.libName);
735
+ }
731
736
  break;
732
737
  }
733
738
  case DependencyStatus.RELINK: {
@@ -736,10 +741,10 @@ async function linkScope(params) {
736
741
  const relinkCommitPath = path.join(storePath, dependency.libName, dependency.commit);
737
742
  const isRelinkGeneral = await resolveLibraryGeneralState(dependency, requestedPlatforms, configVars);
738
743
  if (isRelinkGeneral) {
739
- tx.recordOp('unlink', localPath);
744
+ await tx.recordOp('unlink', localPath);
740
745
  await linker.unlink(localPath);
741
746
  const sharedPath = path.join(relinkCommitPath, '_shared');
742
- tx.recordOp('link', localPath, sharedPath);
747
+ await tx.recordOp('link', localPath, sharedPath);
743
748
  await linker.linkGeneral(localPath, sharedPath);
744
749
  await ensureLinkedRegistryState(dependency.libName, dependency.commit, dependency.branch, dependency.url, [GENERAL_PLATFORM], projectHash, true);
745
750
  generalLibs.add(dependency.libName);
@@ -774,9 +779,9 @@ async function linkScope(params) {
774
779
  }
775
780
  break;
776
781
  }
777
- tx.recordOp('unlink', localPath);
782
+ await tx.recordOp('unlink', localPath);
778
783
  await linker.unlink(localPath);
779
- tx.recordOp('link', localPath, relinkCommitPath);
784
+ await tx.recordOp('link', localPath, relinkCommitPath);
780
785
  await linker.linkLib(localPath, relinkCommitPath, relinkExisting);
781
786
  await ensureLinkedRegistryState(dependency.libName, dependency.commit, dependency.branch, dependency.url, relinkExisting, projectHash, false);
782
787
  if (relinkSupplementResult.downloaded.length > 0) {
@@ -796,7 +801,7 @@ async function linkScope(params) {
796
801
  const isReplaceGeneral = await resolveLibraryGeneralState(dependency, requestedPlatforms, configVars);
797
802
  if (isReplaceGeneral) {
798
803
  const sharedPath = path.join(replaceCommitPath, '_shared');
799
- tx.recordOp('replace', localPath, sharedPath);
804
+ await tx.recordOp('replace', localPath, sharedPath);
800
805
  await linker.linkGeneral(localPath, sharedPath);
801
806
  savedBytes += replaceSize;
802
807
  await ensureLinkedRegistryState(dependency.libName, dependency.commit, dependency.branch, dependency.url, [GENERAL_PLATFORM], projectHash, true);
@@ -832,7 +837,7 @@ async function linkScope(params) {
832
837
  }
833
838
  break;
834
839
  }
835
- tx.recordOp('replace', localPath, replaceCommitPath);
840
+ await tx.recordOp('replace', localPath, replaceCommitPath);
836
841
  await linker.linkLib(localPath, replaceCommitPath, replaceExisting);
837
842
  savedBytes += replaceSize;
838
843
  await ensureLinkedRegistryState(dependency.libName, dependency.commit, dependency.branch, dependency.url, replaceExisting, projectHash, false);
@@ -876,7 +881,7 @@ async function linkScope(params) {
876
881
  const progressTracker = new ProgressTracker({
877
882
  name: ` 移入 Store`, total: absorbSize, showSpeed: true,
878
883
  });
879
- tx.recordOp('absorb', storeCommitPath, localPath);
884
+ await tx.recordOp('absorb', storeCommitPath, localPath);
880
885
  let progressStarted = false;
881
886
  const absorbResult = await store.absorbLib(localPath, finalPlatforms, dependency.libName, dependency.commit, {
882
887
  totalSize: absorbSize,
@@ -923,7 +928,7 @@ async function linkScope(params) {
923
928
  if (absorbSupplementResult.downloaded.length > 0) {
924
929
  absorbLinkPlatforms = [...absorbLinkPlatforms, ...absorbSupplementResult.downloaded];
925
930
  }
926
- tx.recordOp('link', localPath, storeCommitPath);
931
+ await tx.recordOp('link', localPath, storeCommitPath);
927
932
  await linker.linkLib(localPath, storeCommitPath, absorbLinkPlatforms);
928
933
  for (const platform of absorbLinkPlatforms) {
929
934
  const storeKey = registry.getStoreKey(dependency.libName, dependency.commit, platform);
@@ -946,7 +951,7 @@ async function linkScope(params) {
946
951
  break;
947
952
  }
948
953
  const { GENERAL_PLATFORM } = await import('../core/platform.js');
949
- tx.recordOp('link', localPath, sharedPath);
954
+ await tx.recordOp('link', localPath, sharedPath);
950
955
  await linker.linkGeneral(localPath, sharedPath);
951
956
  const storeKey = registry.getStoreKey(dependency.libName, dependency.commit, GENERAL_PLATFORM);
952
957
  if (!registry.getStore(storeKey)) {
@@ -1035,7 +1040,7 @@ async function linkScope(params) {
1035
1040
  clearSatisfiedAutoUnavailablePlatforms(registry, dependency, assessment.satisfiedRequested);
1036
1041
  const filteredDownloaded = assessment.downloadedRequested;
1037
1042
  if (filteredDownloaded.length > 0) {
1038
- tx.recordOp('absorb', linkNewCommitPath, downloadResult.libDir);
1043
+ await tx.recordOp('absorb', linkNewCommitPath, downloadResult.libDir);
1039
1044
  const linkNewAbsorbResult = await store.absorbLib(downloadResult.libDir, filteredDownloaded, dependency.libName, dependency.commit);
1040
1045
  await registerNestedLibraries(linkNewAbsorbResult.nestedLibraries, projectHash);
1041
1046
  const repaired = filteredDownloaded.filter((platform) => repairRequiredSet.has(platform));
@@ -1053,7 +1058,7 @@ async function linkScope(params) {
1053
1058
  const isLinkNewGeneral = await resolveLibraryGeneralState(dependency, requestedPlatforms, configVars);
1054
1059
  if (isLinkNewGeneral) {
1055
1060
  const sharedPath = path.join(linkNewCommitPath, '_shared');
1056
- tx.recordOp('link', localPath, sharedPath);
1061
+ await tx.recordOp('link', localPath, sharedPath);
1057
1062
  await linker.linkGeneral(localPath, sharedPath);
1058
1063
  await ensureLinkedRegistryState(dependency.libName, dependency.commit, dependency.branch, dependency.url, [GENERAL_PLATFORM], projectHash, true);
1059
1064
  generalLibs.add(dependency.libName);
@@ -1085,7 +1090,7 @@ async function linkScope(params) {
1085
1090
  break;
1086
1091
  }
1087
1092
  else {
1088
- tx.recordOp('link', localPath, linkNewCommitPath);
1093
+ await tx.recordOp('link', localPath, linkNewCommitPath);
1089
1094
  await linker.linkLib(localPath, linkNewCommitPath, linkNewExisting);
1090
1095
  await ensureLinkedRegistryState(dependency.libName, dependency.commit, dependency.branch, dependency.url, linkNewExisting, projectHash, false);
1091
1096
  success(`${dependency.libName} (${dependency.commit.slice(0, 7)}) - 创建链接 [${linkNewExisting.join(', ')}]`);
@@ -1154,7 +1159,7 @@ async function linkScope(params) {
1154
1159
  };
1155
1160
  }
1156
1161
  pLog.info(`${dependency.libName} 所有平台已存在,直接链接...`);
1157
- tx.recordOp('link', localPath, storeCommitPath);
1162
+ await tx.recordOp('link', localPath, storeCommitPath);
1158
1163
  await linker.linkLib(localPath, storeCommitPath, existing);
1159
1164
  await ensureLinkedRegistryState(dependency.libName, dependency.commit, dependency.branch, dependency.url, existing, projectHash, false);
1160
1165
  pLog.success(`${dependency.libName} (${dependency.commit.slice(0, 7)}) - 链接完成 [${existing.join(', ')}]`);
@@ -1215,10 +1220,10 @@ async function linkScope(params) {
1215
1220
  platforms: [GENERAL_PLATFORM],
1216
1221
  isGeneral: true,
1217
1222
  });
1218
- tx.recordOp('absorb', storeCommitPath, downloadResult.libDir);
1223
+ await tx.recordOp('absorb', storeCommitPath, downloadResult.libDir);
1219
1224
  await store.absorbGeneral(downloadResult.libDir, dependency.libName, dependency.commit);
1220
1225
  const sharedPath = path.join(storeCommitPath, '_shared');
1221
- tx.recordOp('link', localPath, sharedPath);
1226
+ await tx.recordOp('link', localPath, sharedPath);
1222
1227
  await linker.linkGeneral(localPath, sharedPath);
1223
1228
  await ensureLinkedRegistryState(dependency.libName, dependency.commit, dependency.branch, dependency.url, [GENERAL_PLATFORM], projectHash, true);
1224
1229
  generalLibs.add(dependency.libName);
@@ -1244,7 +1249,7 @@ async function linkScope(params) {
1244
1249
  pLog.warn(`${dependency.libName} 平台 [${newUnavailable.join(', ')}] 远程不存在,已记录`);
1245
1250
  }
1246
1251
  if (filteredDownloaded.length > 0) {
1247
- tx.recordOp('absorb', storeCommitPath, downloadResult.libDir);
1252
+ await tx.recordOp('absorb', storeCommitPath, downloadResult.libDir);
1248
1253
  const downloadAbsorbResult = await store.absorbLib(downloadResult.libDir, filteredDownloaded, dependency.libName, dependency.commit);
1249
1254
  await registerNestedLibraries(downloadAbsorbResult.nestedLibraries, projectHash);
1250
1255
  }
@@ -1261,7 +1266,7 @@ async function linkScope(params) {
1261
1266
  skippedPlatforms: requestedPlatforms,
1262
1267
  };
1263
1268
  }
1264
- tx.recordOp('link', localPath, storeCommitPath);
1269
+ await tx.recordOp('link', localPath, storeCommitPath);
1265
1270
  await linker.linkLib(localPath, storeCommitPath, linkPlatforms);
1266
1271
  await ensureLinkedRegistryState(dependency.libName, dependency.commit, dependency.branch, dependency.url, linkPlatforms, projectHash, false);
1267
1272
  const notLinkedPlatforms = resolveRequestedPlatformsByActual(requestedPlatforms, dependency.sparse, linkPlatforms, configVars).missingRequested;
@@ -1350,7 +1355,7 @@ async function linkScope(params) {
1350
1355
  const thirdPartyDir = path.dirname(configPath);
1351
1356
  for (const action of actions) {
1352
1357
  await processAction(action, nestedContext, configPath, thirdPartyDir, {
1353
- tx, registry, projectHash, projectRoot, dryRun, download, yes,
1358
+ tx, registry, projectHash, projectRoot, storePath, dryRun, download, yes,
1354
1359
  gitLightweightDownload, skipActions: new Set(params.skipActions ?? []),
1355
1360
  generalLibs, downloadedLibs, nestedLinkedDeps,
1356
1361
  });
@@ -1358,7 +1363,7 @@ async function linkScope(params) {
1358
1363
  }
1359
1364
  // 10. 同步 cache 文件
1360
1365
  for (const cacheConfigPath of cacheConfigPaths) {
1361
- await syncCacheFile(cacheConfigPath);
1366
+ await syncCacheFile(cacheConfigPath, storePath, dryRun);
1362
1367
  }
1363
1368
  // 11. 构建返回结果
1364
1369
  const linkedDeps = [];
@@ -2041,7 +2046,10 @@ export async function classifyLinkStatus(localPath, storeCommitPath, isGeneral,
2041
2046
  case 'linked': return 'linked';
2042
2047
  case 'wrong_link':
2043
2048
  case 'broken_link': return 'relink';
2044
- case 'directory': return 'replace';
2049
+ case 'directory':
2050
+ return (await isMaterializedStoreDirectory(localPath, expectedTarget))
2051
+ ? 'linked'
2052
+ : 'replace';
2045
2053
  case 'missing':
2046
2054
  default: return 'link_new';
2047
2055
  }
@@ -2279,7 +2287,7 @@ async function supplementMissingPlatforms(dependency, platforms, registry, tx, o
2279
2287
  if (downloaded.length > 0) {
2280
2288
  const storePath = await store.getStorePath();
2281
2289
  const storeCommitPath = path.join(storePath, dependency.libName, dependency.commit);
2282
- tx.recordOp('absorb', storeCommitPath, downloadResult.libDir);
2290
+ await tx.recordOp('absorb', storeCommitPath, downloadResult.libDir);
2283
2291
  const supplementAbsorbResult = await store.absorbLib(downloadResult.libDir, downloaded, dependency.libName, dependency.commit);
2284
2292
  // 收集嵌套依赖(由调用者负责注册)
2285
2293
  result.nestedLibraries.push(...supplementAbsorbResult.nestedLibraries);
@@ -2377,24 +2385,69 @@ function getStatusKey(status) {
2377
2385
  * 同步配置文件到 cache 目录
2378
2386
  * 使 cmake 的 checkValid.js 检测能够通过
2379
2387
  */
2380
- async function syncCacheFile(configPath) {
2388
+ async function syncCacheFile(configPath, storePath, dryRun = false) {
2381
2389
  const configDir = path.dirname(configPath);
2382
2390
  const cacheDir = path.join(configDir, '.cache');
2383
2391
  const configFileName = path.basename(configPath);
2384
2392
  const cachePath = path.join(cacheDir, configFileName);
2393
+ const tempCachePath = path.join(cacheDir, `.${configFileName}.tanmi-dock-${process.pid}-${crypto.randomBytes(4).toString('hex')}.tmp`);
2394
+ let tempCacheCreated = false;
2395
+ debug(`[link-cache] 同步检查: config=${configPath}, cache=${cachePath}, store=${storePath}, dryRun=${dryRun}`);
2396
+ if (dryRun) {
2397
+ debug(`[link-cache] 预演跳过写入: config=${configPath}, cache=${cachePath}`);
2398
+ return;
2399
+ }
2385
2400
  try {
2401
+ await assertPathOutsideStore(cacheDir, storePath, 'sync-action-cache-directory');
2386
2402
  // 确保 .cache 目录存在
2387
2403
  await fs.mkdir(cacheDir, { recursive: true });
2388
- // 复制配置文件到 cache
2389
- await fs.copyFile(configPath, cachePath);
2404
+ // 文件级符号链接必须先删除链接本身,避免 copyFile 跟随链接覆盖 Store。
2405
+ try {
2406
+ const cacheStat = await fs.lstat(cachePath);
2407
+ if (cacheStat.isDirectory()) {
2408
+ throw new Error(`cache 目标是目录: ${cachePath}`);
2409
+ }
2410
+ if (cacheStat.isSymbolicLink()) {
2411
+ const previousTarget = await fs.readlink(cachePath).catch(() => '(unreadable)');
2412
+ await fs.unlink(cachePath);
2413
+ debug(`[link-cache] 已移除旧 cache 符号链接: cache=${cachePath}, previousTarget=${previousTarget}`);
2414
+ }
2415
+ }
2416
+ catch (err) {
2417
+ if (err.code !== 'ENOENT')
2418
+ throw err;
2419
+ }
2420
+ await assertPathOutsideStore(cachePath, storePath, 'sync-action-cache-file');
2421
+ const tempHandle = await fs.open(tempCachePath, 'wx');
2422
+ await tempHandle.close();
2423
+ tempCacheCreated = true;
2424
+ await fs.copyFile(configPath, tempCachePath);
2425
+ try {
2426
+ await fs.rename(tempCachePath, cachePath);
2427
+ }
2428
+ catch (err) {
2429
+ const code = err.code;
2430
+ if (code !== 'EEXIST' && code !== 'EPERM')
2431
+ throw err;
2432
+ await fs.rm(cachePath, { force: true });
2433
+ await fs.rename(tempCachePath, cachePath);
2434
+ }
2390
2435
  if (process.env.VERBOSE) {
2391
2436
  info(`已同步 cache: ${configFileName} -> ${path.relative(configDir, cachePath)}`);
2392
2437
  }
2438
+ debug(`[link-cache] 同步完成: config=${configPath}, cache=${cachePath}, state=written`);
2393
2439
  }
2394
2440
  catch (err) {
2395
2441
  // cache 同步失败不应阻塞主流程,仅警告
2396
2442
  warn(`cache 同步失败: ${configFileName} -> ${path.relative(configDir, cachePath)}: ${err.message}`);
2397
2443
  }
2444
+ finally {
2445
+ if (tempCacheCreated) {
2446
+ await fs.rm(tempCachePath, { force: true }).catch((err) => {
2447
+ debug(`[link-cache] 临时文件清理失败: temp=${tempCachePath}, message=${err.message}`);
2448
+ });
2449
+ }
2450
+ }
2398
2451
  }
2399
2452
  /**
2400
2453
  * 处理单个 action(嵌套依赖)
@@ -2463,7 +2516,21 @@ async function processAction(action, context, parentConfigPath, parentTargetDir,
2463
2516
  info(`${indent} 嵌套 action 命中 ${dependencies.length} 个依赖,继承平台: ${formatActionPlatforms(plan.inheritedPlatforms)}, 生效平台: ${formatActionPlatforms(plan.effectiveCodepacPlatforms)}`);
2464
2517
  // 5. 合并变量
2465
2518
  const mergedVars = { ...context.vars, ...vars };
2466
- // 6. 处理这些依赖(targetDir 指定嵌套依赖的目标目录)
2519
+ // 6. 写入前分别保护配置目录和目标目录。
2520
+ const configPreparation = await prepareWritableDirectory(path.dirname(plan.nestedConfigPath), {
2521
+ projectRoot: options.projectRoot,
2522
+ storePath: options.storePath,
2523
+ reason: `action-config:${plan.actionName ?? 'unnamed'}:depth-${context.depth}`,
2524
+ transaction: options.tx,
2525
+ dryRun: options.dryRun,
2526
+ });
2527
+ const targetPreparation = await prepareWritableDirectory(plan.nestedTargetDir, {
2528
+ projectRoot: options.projectRoot,
2529
+ storePath: options.storePath,
2530
+ reason: `action-target:${plan.actionName ?? 'unnamed'}:depth-${context.depth}`,
2531
+ transaction: options.tx,
2532
+ dryRun: options.dryRun,
2533
+ });
2467
2534
  await linkNestedDependencies(dependencies, {
2468
2535
  targetDir: plan.nestedTargetDir,
2469
2536
  nestedConfigPath: plan.nestedConfigPath,
@@ -2472,7 +2539,7 @@ async function processAction(action, context, parentConfigPath, parentTargetDir,
2472
2539
  indent,
2473
2540
  });
2474
2541
  // 同步嵌套配置文件的 cache(兼容 checkValid.js 检测)
2475
- await syncCacheFile(plan.nestedConfigPath);
2542
+ await syncCacheFile(plan.nestedConfigPath, options.storePath, options.dryRun);
2476
2543
  // 7. 递归处理嵌套 actions(如果没有 disable_action)
2477
2544
  if (!plan.parsed.disableAction && nestedActions.length > 0) {
2478
2545
  const nestedContext = {
@@ -2489,6 +2556,10 @@ async function processAction(action, context, parentConfigPath, parentTargetDir,
2489
2556
  else if (plan.parsed.disableAction && nestedActions.length > 0) {
2490
2557
  info(`${indent} 跳过后续嵌套 action: name=${plan.actionName ?? '(未命名)'}, 原因=disable_action`);
2491
2558
  }
2559
+ const materializedPaths = [
2560
+ ...new Set([...configPreparation.materializedPaths, ...targetPreparation.materializedPaths]),
2561
+ ];
2562
+ debug(`${indent}CodePac action 完成: name=${plan.actionName ?? '(未命名)'}, dependencies=${dependencies.length}, nestedActions=${nestedActions.length}, materialized=${materializedPaths.join(',') || 'none'}, output=${plan.nestedTargetDir}`);
2492
2563
  }
2493
2564
  function formatActionPlatforms(platforms) {
2494
2565
  return platforms && platforms.length > 0 ? platforms.join(',') : 'all';
@@ -2555,6 +2626,9 @@ async function linkNestedDependencies(dependencies, params) {
2555
2626
  const { targetDir: nestedTargetDir, context, options, indent } = params;
2556
2627
  const { tx, registry, projectHash, projectRoot, dryRun, download, generalLibs, downloadedLibs, nestedLinkedDeps } = options;
2557
2628
  const { platforms, vars } = context;
2629
+ if (!dryRun) {
2630
+ await assertPathOutsideStore(nestedTargetDir, options.storePath, 'link-nested-dependencies');
2631
+ }
2558
2632
  for (const dep of dependencies) {
2559
2633
  const localPath = path.join(nestedTargetDir, dep.libName);
2560
2634
  const storePath = await store.getStorePath();
@@ -2666,7 +2740,7 @@ async function linkNestedDependencies(dependencies, params) {
2666
2740
  clearSatisfiedAutoUnavailablePlatforms(registry, dep, assessment.satisfiedRequested);
2667
2741
  if (assessment.downloadedRequested.length > 0) {
2668
2742
  // 有平台内容 → 吸收平台目录,重新分类为平台库
2669
- tx.recordOp('absorb', storeCommitPath, downloadResult.libDir);
2743
+ await tx.recordOp('absorb', storeCommitPath, downloadResult.libDir);
2670
2744
  const nestedAbsorbResult = await store.absorbLib(downloadResult.libDir, assessment.downloadedRequested, dep.libName, dep.commit);
2671
2745
  await registerNestedLibraries(nestedAbsorbResult.nestedLibraries, projectHash);
2672
2746
  existingPlatforms.push(...assessment.downloadedRequested);
@@ -2716,7 +2790,7 @@ async function linkNestedDependencies(dependencies, params) {
2716
2790
  const allPlatforms = [...existingPlatforms, ...supplementResult.downloaded];
2717
2791
  linkedPlatforms = allPlatforms;
2718
2792
  if (!dryRun) {
2719
- tx.recordOp('link', localPath, storeCommitPath);
2793
+ await tx.recordOp('link', localPath, storeCommitPath);
2720
2794
  await linker.linkLib(localPath, storeCommitPath, allPlatforms);
2721
2795
  }
2722
2796
  success(`${indent} ${dep.libName} - 重新链接完成 [${allPlatforms.join(', ')}]`);
@@ -2756,7 +2830,7 @@ async function linkNestedDependencies(dependencies, params) {
2756
2830
  }
2757
2831
  if (isGeneral) {
2758
2832
  const sharedPath = path.join(storeCommitPath, '_shared');
2759
- tx.recordOp('link', localPath, sharedPath);
2833
+ await tx.recordOp('link', localPath, sharedPath);
2760
2834
  await linker.linkGeneral(localPath, sharedPath);
2761
2835
  await ensureLinkedRegistryState(dep.libName, dep.commit, dep.branch, dep.url, [GENERAL_PLATFORM], projectHash, true);
2762
2836
  generalLibs.add(dep.libName);
@@ -2782,7 +2856,7 @@ async function linkNestedDependencies(dependencies, params) {
2782
2856
  if (supplementResult.downloaded.length > 0) {
2783
2857
  success(`${indent} ${dep.libName} - 补充平台 [${supplementResult.downloaded.join(', ')}]`);
2784
2858
  }
2785
- tx.recordOp('link', localPath, storeCommitPath);
2859
+ await tx.recordOp('link', localPath, storeCommitPath);
2786
2860
  await linker.linkLib(localPath, storeCommitPath, allPlatforms);
2787
2861
  await ensureLinkedRegistryState(dep.libName, dep.commit, dep.branch, dep.url, allPlatforms, projectHash, false);
2788
2862
  // 记录到 nestedLinkedDeps
@@ -2846,10 +2920,10 @@ async function linkNestedDependencies(dependencies, params) {
2846
2920
  platforms: [GENERAL_PLATFORM],
2847
2921
  isGeneral: true,
2848
2922
  });
2849
- tx.recordOp('absorb', storeCommitPath, downloadResult.libDir);
2923
+ await tx.recordOp('absorb', storeCommitPath, downloadResult.libDir);
2850
2924
  await store.absorbGeneral(downloadResult.libDir, dep.libName, dep.commit);
2851
2925
  const sharedPath = path.join(storeCommitPath, '_shared');
2852
- tx.recordOp('link', localPath, sharedPath);
2926
+ await tx.recordOp('link', localPath, sharedPath);
2853
2927
  await linker.linkGeneral(localPath, sharedPath);
2854
2928
  await ensureLinkedRegistryState(dep.libName, dep.commit, dep.branch, dep.url, [GENERAL_PLATFORM], projectHash, true);
2855
2929
  generalLibs.add(dep.libName);
@@ -2870,10 +2944,10 @@ async function linkNestedDependencies(dependencies, params) {
2870
2944
  }
2871
2945
  else if (assessment.downloadedRequested.length > 0) {
2872
2946
  // 平台库处理
2873
- tx.recordOp('absorb', storeCommitPath, downloadResult.libDir);
2947
+ await tx.recordOp('absorb', storeCommitPath, downloadResult.libDir);
2874
2948
  const nestedAbsorbResult = await store.absorbLib(downloadResult.libDir, assessment.downloadedRequested, dep.libName, dep.commit);
2875
2949
  await registerNestedLibraries(nestedAbsorbResult.nestedLibraries, projectHash);
2876
- tx.recordOp('link', localPath, storeCommitPath);
2950
+ await tx.recordOp('link', localPath, storeCommitPath);
2877
2951
  await linker.linkLib(localPath, storeCommitPath, assessment.downloadedRequested);
2878
2952
  await ensureLinkedRegistryState(dep.libName, dep.commit, dep.branch, dep.url, assessment.downloadedRequested, projectHash, false);
2879
2953
  downloadedLibs.push(dep.libName);