@alpsckr/unitycli 0.4.0 → 0.4.2

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 (98) hide show
  1. package/README.md +111 -299
  2. package/contracts/init-contract.json +64 -0
  3. package/contracts/instances-contract.json +49 -0
  4. package/contracts/package-assembly-closure.json +33 -0
  5. package/contracts/package-files.json +1953 -0
  6. package/dist/bridge/session-supervisor.d.ts +2 -0
  7. package/dist/bridge/session-supervisor.js +52 -19
  8. package/dist/bridge/session-supervisor.js.map +1 -1
  9. package/dist/cli-commands/command.js +33 -15
  10. package/dist/cli-commands/command.js.map +1 -1
  11. package/dist/cli-commands/init-discovery.d.ts +11 -0
  12. package/dist/cli-commands/init-discovery.js +185 -0
  13. package/dist/cli-commands/init-discovery.js.map +1 -0
  14. package/dist/cli-commands/init-help.d.ts +3 -0
  15. package/dist/cli-commands/init-help.js +41 -0
  16. package/dist/cli-commands/init-help.js.map +1 -0
  17. package/dist/cli-commands/init-skill.d.ts +10 -0
  18. package/dist/cli-commands/init-skill.js +171 -0
  19. package/dist/cli-commands/init-skill.js.map +1 -0
  20. package/dist/cli-commands/instances.d.ts +4 -0
  21. package/dist/cli-commands/instances.js +90 -0
  22. package/dist/cli-commands/instances.js.map +1 -0
  23. package/dist/cli-commands/package.d.ts +2 -2
  24. package/dist/cli-commands/package.js +97 -130
  25. package/dist/cli-commands/package.js.map +1 -1
  26. package/dist/cli-commands/skill-installer.d.ts +29 -13
  27. package/dist/cli-commands/skill-installer.js +127 -88
  28. package/dist/cli-commands/skill-installer.js.map +1 -1
  29. package/dist/cli-commands/types.d.ts +4 -0
  30. package/dist/cli.d.ts +1 -1
  31. package/dist/cli.js +46 -37
  32. package/dist/cli.js.map +1 -1
  33. package/dist/contracts/command-contract.js +1 -0
  34. package/dist/contracts/command-contract.js.map +1 -1
  35. package/dist/doctor.js +1 -1
  36. package/dist/doctor.js.map +1 -1
  37. package/dist/instances.d.ts +1 -1
  38. package/dist/instances.js +63 -38
  39. package/dist/instances.js.map +1 -1
  40. package/dist/output.d.ts +4 -4
  41. package/dist/output.js +3 -4
  42. package/dist/output.js.map +1 -1
  43. package/dist/package-manager.d.ts +47 -2
  44. package/dist/package-manager.js +267 -16
  45. package/dist/package-manager.js.map +1 -1
  46. package/dist/tools/.generated-schemas.js +2 -2
  47. package/dist/tools/.generated-schemas.js.map +1 -1
  48. package/dist/tools/commands.js +11 -4
  49. package/dist/tools/commands.js.map +1 -1
  50. package/dist/tools/dispatcher.js +1 -1
  51. package/dist/tools/dispatcher.js.map +1 -1
  52. package/dist/tools/tools/group-list.d.ts +4 -8
  53. package/dist/tools/tools/group-list.js +12 -13
  54. package/dist/tools/tools/group-list.js.map +1 -1
  55. package/dist/tools/tools/pagination.d.ts +17 -0
  56. package/dist/tools/tools/pagination.js +28 -0
  57. package/dist/tools/tools/pagination.js.map +1 -0
  58. package/dist/tools/tools/search.d.ts +3 -5
  59. package/dist/tools/tools/search.js +12 -12
  60. package/dist/tools/tools/search.js.map +1 -1
  61. package/dist/unity-project-processes.d.ts +7 -0
  62. package/dist/unity-project-processes.js +54 -0
  63. package/dist/unity-project-processes.js.map +1 -1
  64. package/docs/README.md +7 -4
  65. package/docs/architecture.md +103 -0
  66. package/docs/availability-differences.md +1 -1
  67. package/docs/command-execution.md +6 -1
  68. package/docs/development.md +91 -0
  69. package/docs/error-codes.md +21 -5
  70. package/docs/extensions.md +13 -13
  71. package/docs/target-and-instance.md +6 -4
  72. package/docs/tool-discovery.md +12 -10
  73. package/docs/tool-execution.md +3 -2
  74. package/package.json +10 -4
  75. package/schemas/init.schema.json +209 -0
  76. package/schemas/instances.schema.json +104 -0
  77. package/skills/unitycli/SKILL.md +18 -6
  78. package/skills/unitycli/manifest.json +2 -2
  79. package/skills/unitycli/references/custom-tools.md +3 -3
  80. package/skills/unitycli/references/operation-protocol.md +6 -4
  81. package/skills/unitycli/reports/output_quality_scorecard.md +1 -1
  82. package/skills/unitycli/reports/trust-report.md +1 -1
  83. package/unitypkg/Editor/Bridge/BridgeCommandRegistry.cs +1 -0
  84. package/unitypkg/Editor/Bridge/BridgeWorkQueue.cs +18 -6
  85. package/unitypkg/Editor/Infrastructure/CodeExecSupport.cs +8 -77
  86. package/unitypkg/Editor/Infrastructure/UnityCliInstanceCleanup.cs +295 -0
  87. package/unitypkg/Editor/Infrastructure/UnityCliInstanceRegistry.cs +166 -10
  88. package/unitypkg/Editor/Infrastructure/UnityCliOperationCoordinator.cs +1 -0
  89. package/unitypkg/Editor/UnityCliBridge.cs +5 -1
  90. package/unitypkg/Tests/Editor/UnityCliInstanceCleanupTests.cs +232 -0
  91. package/unitypkg/Tests/Editor/UnityCliInstancePublicationRetryTests.cs +143 -0
  92. package/unitypkg/UnityCli.Editor.asmdef +1 -5
  93. package/unitypkg/package.json +1 -1
  94. package/unitypkg/Editor/Roslyn/LICENSE.txt +0 -23
  95. package/unitypkg/Editor/Roslyn/Microsoft.CodeAnalysis.CSharp.dll +0 -0
  96. package/unitypkg/Editor/Roslyn/Microsoft.CodeAnalysis.dll +0 -0
  97. package/unitypkg/Editor/Roslyn/System.Collections.Immutable.dll +0 -0
  98. package/unitypkg/Editor/Roslyn/System.Reflection.Metadata.dll +0 -0
@@ -210,13 +210,17 @@ async function removeScopedRegistryLocked(projectPath, name, options) {
210
210
  }
211
211
  return { manifestPath: paths.manifestPath, removed, matchedPackages, forced: options.force === true, dryRun: true, blocked, plannedEffects };
212
212
  }
213
- /** 在任何 init filesystem effect 前冻结 Bridge 源、目标与哈希。 */
213
+ /** 在任何 init filesystem effect 前验证并冻结 canonical Bridge 源、目标与哈希。 */
214
214
  export async function preparePackageBridgeInstall(projectPath) {
215
215
  const paths = packagePaths(projectPath);
216
216
  const sourcePath = packageSourcePath();
217
- const packageManifest = await readPackageManifest(sourcePath);
217
+ const canonical = await verifyPackageSourceIntegrity(sourcePath);
218
+ const assemblyInspection = await inspectPackageAssemblyConflicts(paths.projectPath);
219
+ if (assemblyInspection.status === "conflict")
220
+ throw packageAssemblyConflictError(assemblyInspection);
221
+ const packageManifest = canonical.packageManifest;
218
222
  const projectHash = await hashProjectPath(paths.projectPath);
219
- const sourceHash = await hashSourcePath(sourcePath);
223
+ const sourceHash = canonical.sourceHash;
220
224
  const bridgePath = join(paths.projectPath, "Packages", bridgePackageName);
221
225
  return {
222
226
  manifestPath: paths.manifestPath,
@@ -389,9 +393,13 @@ async function ensurePackageBridgeLocked(projectPath, prepared, runtime = {}) {
389
393
  const paths = packagePaths(projectPath);
390
394
  const sourcePath = packageSourcePath();
391
395
  const plan = prepared ?? await preparePackageBridgeInstall(paths.projectPath);
392
- const packageManifest = await readPackageManifest(sourcePath);
393
- // init plan sourceHash 是 effect 前冻结证据;任何目标写入前必须重新散列并严格匹配。
394
- const freshSourceHash = await hashSourcePath(sourcePath);
396
+ // init plan sourceHash 是 effect 前冻结证据;任何目标写入前必须重验 canonical 清单与全部文件字节。
397
+ const canonical = await verifyPackageSourceIntegrity(sourcePath);
398
+ const assemblyInspection = await inspectPackageAssemblyConflicts(paths.projectPath);
399
+ if (assemblyInspection.status === "conflict")
400
+ throw packageAssemblyConflictError(assemblyInspection);
401
+ const packageManifest = canonical.packageManifest;
402
+ const freshSourceHash = canonical.sourceHash;
395
403
  if (plan.sourceHash !== freshSourceHash || plan.packageVersion !== packageManifest.version) {
396
404
  throw new PackageManagerError("INIT_PLAN_STALE", "Bridge package source changed after init prevalidation.", ["Submit init again so it can build a fresh transaction plan."]);
397
405
  }
@@ -403,7 +411,7 @@ async function ensurePackageBridgeLocked(projectPath, prepared, runtime = {}) {
403
411
  const publishDeadlineMs = runtime.absoluteDeadlineMs ?? Date.now() + 5_000;
404
412
  const abandonedPreviousCount = await recoverAbandonedPackagePublish(targetPath, publishDeadlineMs, runtime);
405
413
  // Check existing installation: skip if same version & hash
406
- const expectedManagedFiles = await listManagedSourceFiles(sourcePath);
414
+ const expectedManagedFiles = canonical.files;
407
415
  const expectedManagedFilesSha256 = managedFilesSha256(expectedManagedFiles);
408
416
  const targetExists = await directoryExists(targetPath);
409
417
  // 包目录不存在时 metadata 只是孤立残留;普通 init 可用当前完整投影替换它。
@@ -529,6 +537,10 @@ async function ensurePackageBridgeLocked(projectPath, prepared, runtime = {}) {
529
537
  ]);
530
538
  }
531
539
  }
540
+ if (isTransientRenameError(error)) {
541
+ const code = String(error.code ?? "EPERM");
542
+ throw new PackageManagerError("PACKAGE_DIRECTORY_LOCKED", `Windows kept the existing UnityCLI package directory open while replacement was attempted (${code}).`, ["Close the exact target Unity Editor and any process using the package directory, then rerun the same init package command."], { operation: "replace-existing-package", nativeCode: code });
543
+ }
532
544
  throw error;
533
545
  }
534
546
  finally {
@@ -635,7 +647,8 @@ async function verifyCurrentPackageBridge(projectPath, code, nextActions) {
635
647
  const { packageManifest, installMetadata } = await readInstalledPackageBridge(paths.projectPath, code, nextActions);
636
648
  const projectHash = await hashProjectPath(paths.projectPath);
637
649
  const sourcePath = packageSourcePath();
638
- const sourceHash = await hashSourcePath(sourcePath);
650
+ const canonical = await verifyPackageSourceIntegrity(sourcePath);
651
+ const sourceHash = canonical.sourceHash;
639
652
  if (installMetadata.packageVersion !== packageManifest.version || installMetadata.sourceHash !== sourceHash
640
653
  || installMetadata.projectHash !== projectHash || installMetadata.cliCompatibilityRange !== cliCompatibilityRange)
641
654
  throw new PackageManagerError(code, `${bridgePackageName} install metadata does not match the current project and bundled source.`, nextActions);
@@ -644,8 +657,79 @@ async function verifyCurrentPackageBridge(projectPath, code, nextActions) {
644
657
  throw new PackageManagerError(code, `${bridgePackageName} managed file projection does not match the bundled source.`, nextActions);
645
658
  return { paths, targetPath, installMetadataPath, fileManifestPath, packageManifest, installMetadata, projectHash, sourceHash, managedFiles };
646
659
  }
660
+ /** 只读检查 Bridge 投影,供 `init package --check` 区分缺失、损坏与版本漂移。 */
661
+ export async function inspectPackageBridge(projectPath) {
662
+ const paths = packagePaths(projectPath);
663
+ const sourcePath = packageSourcePath();
664
+ const canonical = await verifyPackageSourceIntegrity(sourcePath);
665
+ const availableVersion = canonical.packageManifest.version;
666
+ const assemblyInspection = await inspectPackageAssemblyConflicts(paths.projectPath);
667
+ const targetPath = join(paths.projectPath, "Packages", bridgePackageName);
668
+ if (assemblyInspection.status === "conflict") {
669
+ return { status: "conflict", projectPath: paths.projectPath, bridgePath: targetPath, sourceHash: canonical.sourceHash, availableVersion, reasonCode: "PACKAGE_ASSEMBLY_CONFLICT", message: `${bridgePackageName} is blocked by a protected assembly name from another Package.`, conflicts: assemblyInspection.conflicts };
670
+ }
671
+ if (!await directoryExists(targetPath)) {
672
+ return { status: "missing", projectPath: paths.projectPath, bridgePath: targetPath, sourceHash: canonical.sourceHash, availableVersion, reasonCode: "PACKAGE_NOT_INSTALLED", message: `${bridgePackageName} is not installed.` };
673
+ }
674
+ const installMetadataPath = join(paths.projectPath, "Packages", installMetadataFileName);
675
+ const fileManifestPath = join(targetPath, fileManifestFileName);
676
+ let packageManifest;
677
+ let installMetadata;
678
+ try {
679
+ packageManifest = await readPackageManifest(targetPath);
680
+ installMetadata = await readOptionalInstallMetadata(installMetadataPath);
681
+ }
682
+ catch (error) {
683
+ return { status: "damaged", projectPath: paths.projectPath, bridgePath: targetPath, sourceHash: canonical.sourceHash, availableVersion, reasonCode: "PACKAGE_INSTALL_DAMAGED", message: error instanceof Error ? error.message : String(error) };
684
+ }
685
+ const installedVersion = packageManifest.version;
686
+ if (!installMetadata || installMetadata.packageName !== bridgePackageName || installMetadata.installMode !== "copy") {
687
+ return { status: "damaged", projectPath: paths.projectPath, bridgePath: targetPath, sourceHash: canonical.sourceHash, availableVersion, installedVersion, reasonCode: "PACKAGE_INSTALL_DAMAGED", message: `${bridgePackageName} install metadata is missing or does not describe a managed copy.` };
688
+ }
689
+ const projectHash = await hashProjectPath(paths.projectPath);
690
+ if (packageManifest.version !== canonical.packageManifest.version || installMetadata.packageVersion !== canonical.packageManifest.version
691
+ || installMetadata.sourceHash !== canonical.sourceHash || installMetadata.cliCompatibilityRange !== cliCompatibilityRange) {
692
+ return { status: "drifted", projectPath: paths.projectPath, bridgePath: targetPath, sourceHash: canonical.sourceHash, availableVersion, installedVersion, reasonCode: "PACKAGE_INSTALL_DRIFTED", message: `${bridgePackageName} version or source hash differs from the bundled canonical package.` };
693
+ }
694
+ if (installMetadata.projectHash !== projectHash) {
695
+ return { status: "damaged", projectPath: paths.projectPath, bridgePath: targetPath, sourceHash: canonical.sourceHash, availableVersion, installedVersion, reasonCode: "PACKAGE_INSTALL_DAMAGED", message: `${bridgePackageName} install metadata belongs to a different canonical project.` };
696
+ }
697
+ try {
698
+ await readVerifiedManagedFileList(fileManifestPath, installMetadata.managedFilesSha256);
699
+ if (!await managedPackageFilesMatch(sourcePath, targetPath, fileManifestPath)) {
700
+ return { status: "damaged", projectPath: paths.projectPath, bridgePath: targetPath, sourceHash: canonical.sourceHash, availableVersion, installedVersion, reasonCode: "PACKAGE_INSTALL_DAMAGED", message: `${bridgePackageName} managed files are missing or do not match canonical bytes.` };
701
+ }
702
+ }
703
+ catch (error) {
704
+ return { status: "damaged", projectPath: paths.projectPath, bridgePath: targetPath, sourceHash: canonical.sourceHash, availableVersion, installedVersion, reasonCode: "PACKAGE_INSTALL_DAMAGED", message: error instanceof Error ? error.message : String(error) };
705
+ }
706
+ return {
707
+ status: "installed",
708
+ projectPath: paths.projectPath,
709
+ bridgePath: targetPath,
710
+ sourceHash: canonical.sourceHash,
711
+ availableVersion,
712
+ installedVersion,
713
+ install: {
714
+ manifestPath: paths.manifestPath,
715
+ bridgePath: targetPath,
716
+ dependencyName: bridgePackageName,
717
+ packageVersion: canonical.packageManifest.version,
718
+ sourceHash: canonical.sourceHash,
719
+ projectHash,
720
+ installMode: "copy",
721
+ installTime: installMetadata.installTime,
722
+ cliCompatibilityRange,
723
+ installMetadataPath,
724
+ fileManifestPath,
725
+ },
726
+ };
727
+ }
647
728
  export async function checkPackageBridge(projectPath) {
648
- const verified = await verifyCurrentPackageBridge(projectPath, "INIT_CHECK_FAILED", ["Run unitycli init --project <path> to restore the bundled package projection."]);
729
+ const assemblyInspection = await inspectPackageAssemblyConflicts(projectPath);
730
+ if (assemblyInspection.status === "conflict")
731
+ throw packageAssemblyConflictError(assemblyInspection);
732
+ const verified = await verifyCurrentPackageBridge(projectPath, "INIT_CHECK_FAILED", ["Run unitycli init package --project <UnityProject> to restore the bundled package projection."]);
649
733
  const { paths, targetPath, installMetadataPath, fileManifestPath, packageManifest, installMetadata, projectHash, sourceHash } = verified;
650
734
  return {
651
735
  manifestPath: paths.manifestPath,
@@ -667,7 +751,7 @@ export async function removePackageBridge(projectPath, options = {}) {
667
751
  return withProjectFileMutationLease(resolvedProject, "package-bridge-publish", () => removePackageBridgeLocked(resolvedProject, options));
668
752
  }
669
753
  async function removePackageBridgeLocked(projectPath, options = {}) {
670
- const verificationActions = ["Run unitycli init --project <path> to restore a fully verified bundled package before removal."];
754
+ const verificationActions = ["Run unitycli init package --project <UnityProject> to restore a fully verified bundled package before removal."];
671
755
  const verified = await verifyCurrentPackageBridge(projectPath, "BRIDGE_PACKAGE_REMOVE_UNVERIFIED", verificationActions);
672
756
  const { paths, targetPath, installMetadataPath, fileManifestPath, packageManifest, installMetadata, managedFiles } = verified;
673
757
  const plannedEffects = [
@@ -1044,15 +1128,176 @@ export async function searchRegistry(registry, query) {
1044
1128
  export class PackageManagerError extends Error {
1045
1129
  code;
1046
1130
  nextActions;
1047
- constructor(code, message, nextActions = []) {
1131
+ details;
1132
+ transient;
1133
+ retryAfterMs;
1134
+ constructor(code, message, nextActions = [], details, transient = false, retryAfterMs = 0) {
1048
1135
  super(message);
1049
1136
  this.code = code;
1050
1137
  this.nextActions = nextActions;
1138
+ this.details = details;
1139
+ this.transient = transient;
1140
+ this.retryAfterMs = retryAfterMs;
1051
1141
  }
1052
1142
  }
1053
1143
  function packageSourcePath() {
1054
1144
  return resolve(fileURLToPath(new URL("../unitypkg", import.meta.url)));
1055
1145
  }
1146
+ function packageFilesContractPath() {
1147
+ return resolve(fileURLToPath(new URL("../contracts/package-files.json", import.meta.url)));
1148
+ }
1149
+ function packageAssemblyClosureContractPath() {
1150
+ return resolve(fileURLToPath(new URL("../contracts/package-assembly-closure.json", import.meta.url)));
1151
+ }
1152
+ function packageSourceIntegrityError(detail) {
1153
+ return new PackageManagerError("PACKAGE_SOURCE_INTEGRITY_FAILED", `Bundled ${bridgePackageName} source integrity check failed: ${detail}`, ["Reinstall or upgrade @alpsckr/unitycli; do not copy package files or DLLs from a Unity installation or another project."]);
1154
+ }
1155
+ async function readPackageAssemblyConflictPolicy() {
1156
+ try {
1157
+ const contract = JSON.parse(stripJsonBom(await readFile(packageAssemblyClosureContractPath(), "utf8")));
1158
+ const policy = contract.conflictPolicy;
1159
+ if (contract.schemaVersion !== 1 || contract.packageName !== bridgePackageName || !policy
1160
+ || !Array.isArray(policy.protectedAssemblyFileNames) || policy.protectedAssemblyFileNames.length === 0
1161
+ || !policy.protectedAssemblyFileNames.every((item) => typeof item === "string" && item.toLowerCase().endsWith(".dll"))
1162
+ || !Array.isArray(policy.allowedPackageProviders) || !policy.allowedPackageProviders.every((item) => typeof item === "string" && item.length > 0)
1163
+ || !Number.isSafeInteger(policy.maxDirectories) || policy.maxDirectories <= 0
1164
+ || !Number.isSafeInteger(policy.maxFiles) || policy.maxFiles <= 0
1165
+ || !Number.isSafeInteger(policy.maxConflicts) || policy.maxConflicts <= 0) {
1166
+ throw new Error("assembly conflict policy schema is invalid");
1167
+ }
1168
+ return policy;
1169
+ }
1170
+ catch (error) {
1171
+ throw packageSourceIntegrityError(error instanceof Error ? error.message : String(error));
1172
+ }
1173
+ }
1174
+ function packageCacheProvider(directoryName) {
1175
+ const versionSeparator = directoryName.lastIndexOf("@");
1176
+ return versionSeparator > 0 ? directoryName.slice(0, versionSeparator) : directoryName;
1177
+ }
1178
+ function packageAssemblyScanError(code, message, details) {
1179
+ return new PackageManagerError(code, message, ["Inspect the reported package path, then remove or update the conflicting package before retrying init."], details);
1180
+ }
1181
+ /** 只读扫描项目 Package roots 中会遮蔽 UnityCLI 显式预编译引用的同名程序集。 */
1182
+ export async function inspectPackageAssemblyConflicts(projectPath) {
1183
+ const projectRoot = packagePaths(projectPath).projectPath;
1184
+ const policy = await readPackageAssemblyConflictPolicy();
1185
+ const protectedNames = new Set(policy.protectedAssemblyFileNames.map((item) => item.toLowerCase()));
1186
+ const allowedProviders = new Set(policy.allowedPackageProviders.map((item) => item.toLowerCase()));
1187
+ const conflicts = [];
1188
+ const allowedMatches = [];
1189
+ let scannedDirectories = 0;
1190
+ let scannedFiles = 0;
1191
+ const queue = [];
1192
+ const addPackageRoots = async (root, source) => {
1193
+ let entries;
1194
+ try {
1195
+ entries = await readdir(root, { withFileTypes: true });
1196
+ }
1197
+ catch (error) {
1198
+ const code = error.code;
1199
+ if (code === "ENOENT" || code === "ENOTDIR")
1200
+ return;
1201
+ throw error;
1202
+ }
1203
+ for (const entry of entries.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0)) {
1204
+ if (entry.isSymbolicLink())
1205
+ throw packageAssemblyScanError("PACKAGE_ASSEMBLY_SCAN_UNSAFE", "Package assembly scan found a linked package root.", { path: posix.normalize(relative(projectRoot, join(root, entry.name)).split("\\").join("/")) });
1206
+ if (!entry.isDirectory())
1207
+ continue;
1208
+ const provider = source === "Packages" ? entry.name : packageCacheProvider(entry.name);
1209
+ if (provider.toLowerCase() === bridgePackageName)
1210
+ continue;
1211
+ queue.push({ directory: join(root, entry.name), provider, source });
1212
+ }
1213
+ };
1214
+ await addPackageRoots(join(projectRoot, "Packages"), "Packages");
1215
+ await addPackageRoots(join(projectRoot, "Library", "PackageCache"), "PackageCache");
1216
+ for (let index = 0; index < queue.length; index += 1) {
1217
+ const current = queue[index];
1218
+ scannedDirectories += 1;
1219
+ if (scannedDirectories > policy.maxDirectories)
1220
+ throw packageAssemblyScanError("PACKAGE_ASSEMBLY_SCAN_LIMIT", "Package assembly scan exceeded its directory limit.", { scannedDirectories, scannedFiles });
1221
+ const entries = (await readdir(current.directory, { withFileTypes: true })).sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
1222
+ for (const entry of entries) {
1223
+ const absolute = join(current.directory, entry.name);
1224
+ const relativePath = posix.normalize(relative(projectRoot, absolute).split("\\").join("/"));
1225
+ if (entry.isSymbolicLink())
1226
+ throw packageAssemblyScanError("PACKAGE_ASSEMBLY_SCAN_UNSAFE", "Package assembly scan found a linked entry.", { path: relativePath, provider: current.provider });
1227
+ if (entry.isDirectory()) {
1228
+ queue.push({ ...current, directory: absolute });
1229
+ continue;
1230
+ }
1231
+ if (!entry.isFile())
1232
+ continue;
1233
+ scannedFiles += 1;
1234
+ if (scannedFiles > policy.maxFiles)
1235
+ throw packageAssemblyScanError("PACKAGE_ASSEMBLY_SCAN_LIMIT", "Package assembly scan exceeded its file limit.", { scannedDirectories, scannedFiles });
1236
+ if (!protectedNames.has(entry.name.toLowerCase()))
1237
+ continue;
1238
+ const match = {
1239
+ assemblyFileName: entry.name,
1240
+ provider: current.provider,
1241
+ source: current.source,
1242
+ relativePath,
1243
+ sha256: createHash("sha256").update(await readFile(absolute)).digest("hex"),
1244
+ };
1245
+ if (allowedProviders.has(current.provider.toLowerCase()))
1246
+ allowedMatches.push(match);
1247
+ else {
1248
+ conflicts.push(match);
1249
+ if (conflicts.length >= policy.maxConflicts)
1250
+ throw packageAssemblyScanError("PACKAGE_ASSEMBLY_SCAN_LIMIT", "Package assembly scan reached its conflict limit.", { scannedDirectories, scannedFiles, conflicts });
1251
+ }
1252
+ }
1253
+ }
1254
+ return { status: conflicts.length ? "conflict" : "clear", conflicts, allowedMatches, scannedDirectories, scannedFiles };
1255
+ }
1256
+ function packageAssemblyConflictError(inspection) {
1257
+ return new PackageManagerError("PACKAGE_ASSEMBLY_CONFLICT", `${bridgePackageName} cannot be installed because another Package provides a protected assembly name.`, ["Remove or update the reported package, or use the official allowed provider, then rerun init package."], { conflicts: inspection.conflicts, allowedMatches: inspection.allowedMatches });
1258
+ }
1259
+ /** 安装源必须逐文件匹配 npm artifact 内固定清单;任何异常都在目标 effect 前 fail closed。 */
1260
+ export async function verifyPackageSourceIntegrity(sourcePath, contractPath = packageFilesContractPath()) {
1261
+ try {
1262
+ const contract = JSON.parse(stripJsonBom(await readFile(contractPath, "utf8")));
1263
+ const packageManifest = await readPackageManifest(sourcePath);
1264
+ if (contract.schemaVersion !== 1 || contract.packageName !== bridgePackageName || contract.packageName !== packageManifest.name
1265
+ || contract.packageVersion !== packageManifest.version || !isSha256(contract.sourceHash) || !Array.isArray(contract.files) || contract.files.length === 0) {
1266
+ throw packageSourceIntegrityError("canonical manifest identity or schema is invalid");
1267
+ }
1268
+ const expectedFiles = await listManagedSourceFiles(sourcePath);
1269
+ const identities = new Set();
1270
+ const manifestFiles = [];
1271
+ for (const rawEntry of contract.files) {
1272
+ if (!rawEntry || typeof rawEntry !== "object")
1273
+ throw packageSourceIntegrityError("canonical manifest contains a non-object file entry");
1274
+ const entry = rawEntry;
1275
+ assertManagedRelativeFilePath(entry.path, contractPath);
1276
+ if (!Number.isSafeInteger(entry.size) || entry.size < 0 || !isSha256(entry.sha256))
1277
+ throw packageSourceIntegrityError(`canonical manifest entry is invalid: ${entry.path}`);
1278
+ const identity = entry.path.toLowerCase();
1279
+ if (identities.has(identity))
1280
+ throw packageSourceIntegrityError(`canonical manifest contains a duplicate path: ${entry.path}`);
1281
+ identities.add(identity);
1282
+ manifestFiles.push(entry.path);
1283
+ const content = await readFile(join(sourcePath, entry.path));
1284
+ const sha256 = createHash("sha256").update(content).digest("hex");
1285
+ if (content.length !== entry.size || sha256 !== entry.sha256)
1286
+ throw packageSourceIntegrityError(`canonical file bytes do not match: ${entry.path}`);
1287
+ }
1288
+ if (manifestFiles.length !== expectedFiles.length || manifestFiles.some((file, index) => file !== expectedFiles[index]))
1289
+ throw packageSourceIntegrityError("canonical file set does not match the bundled package directory");
1290
+ const sourceHash = await hashSourcePath(sourcePath);
1291
+ if (sourceHash !== contract.sourceHash)
1292
+ throw packageSourceIntegrityError("canonical package tree hash does not match");
1293
+ return { packageManifest, sourceHash, files: manifestFiles };
1294
+ }
1295
+ catch (error) {
1296
+ if (error instanceof PackageManagerError && error.code === "PACKAGE_SOURCE_INTEGRITY_FAILED")
1297
+ throw error;
1298
+ throw packageSourceIntegrityError(error instanceof Error ? error.message : String(error));
1299
+ }
1300
+ }
1056
1301
  /** 读取并确认项目内 Bridge 包是 init 托管的 copy-mode 安装。 */
1057
1302
  async function readInstalledPackageBridge(projectPath, code, nextActions) {
1058
1303
  const targetPath = join(projectPath, "Packages", bridgePackageName);
@@ -1153,7 +1398,7 @@ async function readInstallMetadata(path) {
1153
1398
  try {
1154
1399
  const metadata = JSON.parse(stripJsonBom(await readFile(path, "utf8")));
1155
1400
  if (typeof metadata.packageName !== "string" || typeof metadata.packageVersion !== "string" || !isSha256(metadata.sourceHash) || !isSha256(metadata.managedFilesSha256) || typeof metadata.projectHash !== "string" || metadata.installMode !== "copy" || typeof metadata.installTime !== "string" || typeof metadata.cliCompatibilityRange !== "string") {
1156
- throw new PackageManagerError("INIT_CHECK_FAILED", `Install metadata is invalid: ${path}`, ["Run unitycli init --project <path> to refresh install metadata."]);
1401
+ throw new PackageManagerError("INIT_CHECK_FAILED", `Install metadata is invalid: ${path}`, ["Run unitycli init package --project <UnityProject> to refresh install metadata."]);
1157
1402
  }
1158
1403
  return metadata;
1159
1404
  }
@@ -1163,10 +1408,10 @@ async function readInstallMetadata(path) {
1163
1408
  }
1164
1409
  const nodeError = error;
1165
1410
  if (nodeError.code === "ENOENT") {
1166
- throw new PackageManagerError("INIT_CHECK_FAILED", `Install metadata not found: ${path}`, ["Run unitycli init --project <path> to create install metadata."]);
1411
+ throw new PackageManagerError("INIT_CHECK_FAILED", `Install metadata not found: ${path}`, ["Run unitycli init package --project <UnityProject> to create install metadata."]);
1167
1412
  }
1168
1413
  if (error instanceof SyntaxError) {
1169
- throw new PackageManagerError("INIT_CHECK_FAILED", `Install metadata is invalid JSON: ${path}`, ["Run unitycli init --project <path> to refresh install metadata."]);
1414
+ throw new PackageManagerError("INIT_CHECK_FAILED", `Install metadata is invalid JSON: ${path}`, ["Run unitycli init package --project <UnityProject> to refresh install metadata."]);
1170
1415
  }
1171
1416
  throw error;
1172
1417
  }
@@ -1387,12 +1632,18 @@ async function managedPackageFilesMatch(sourcePath, targetPath, fileManifestPath
1387
1632
  async function listManagedSourceFiles(sourcePath) {
1388
1633
  const files = [];
1389
1634
  const visit = async (dir) => {
1390
- for (const entry of await readdir(dir, { withFileTypes: true })) {
1635
+ const entries = (await readdir(dir, { withFileTypes: true })).sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
1636
+ for (const entry of entries) {
1391
1637
  const fullPath = join(dir, entry.name);
1638
+ const relPath = posix.normalize(relative(sourcePath, fullPath).split("\\").join("/"));
1639
+ if (entry.isSymbolicLink())
1640
+ throw packageSourceIntegrityError(`canonical package source contains a link: ${relPath}`);
1392
1641
  if (entry.isDirectory())
1393
1642
  await visit(fullPath);
1394
1643
  else if (entry.isFile())
1395
- files.push(posix.normalize(relative(sourcePath, fullPath).split("\\").join("/")));
1644
+ files.push(relPath);
1645
+ else
1646
+ throw packageSourceIntegrityError(`canonical package source contains an unsupported entry: ${relPath}`);
1396
1647
  }
1397
1648
  };
1398
1649
  await visit(sourcePath);