@alpsckr/unitycli 0.4.0 → 0.4.1

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 (95) hide show
  1. package/README.md +109 -300
  2. package/contracts/init-contract.json +57 -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 +4 -0
  12. package/dist/cli-commands/init-discovery.js +175 -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 +37 -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 +105 -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 +57 -137
  25. package/dist/cli-commands/package.js.map +1 -1
  26. package/dist/cli-commands/skill-installer.d.ts +14 -6
  27. package/dist/cli-commands/skill-installer.js +37 -38
  28. package/dist/cli-commands/skill-installer.js.map +1 -1
  29. package/dist/cli-commands/types.d.ts +1 -0
  30. package/dist/cli.d.ts +1 -1
  31. package/dist/cli.js +44 -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 +43 -2
  44. package/dist/package-manager.js +255 -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/docs/README.md +7 -4
  62. package/docs/architecture.md +103 -0
  63. package/docs/availability-differences.md +1 -1
  64. package/docs/command-execution.md +6 -1
  65. package/docs/development.md +91 -0
  66. package/docs/error-codes.md +5 -5
  67. package/docs/extensions.md +11 -13
  68. package/docs/target-and-instance.md +6 -4
  69. package/docs/tool-discovery.md +12 -10
  70. package/docs/tool-execution.md +3 -2
  71. package/package.json +10 -4
  72. package/schemas/init.schema.json +124 -0
  73. package/schemas/instances.schema.json +104 -0
  74. package/skills/unitycli/SKILL.md +18 -6
  75. package/skills/unitycli/manifest.json +2 -2
  76. package/skills/unitycli/references/custom-tools.md +3 -3
  77. package/skills/unitycli/references/operation-protocol.md +6 -4
  78. package/skills/unitycli/reports/output_quality_scorecard.md +1 -1
  79. package/skills/unitycli/reports/trust-report.md +1 -1
  80. package/unitypkg/Editor/Bridge/BridgeCommandRegistry.cs +1 -0
  81. package/unitypkg/Editor/Bridge/BridgeWorkQueue.cs +18 -6
  82. package/unitypkg/Editor/Infrastructure/CodeExecSupport.cs +8 -77
  83. package/unitypkg/Editor/Infrastructure/UnityCliInstanceCleanup.cs +295 -0
  84. package/unitypkg/Editor/Infrastructure/UnityCliInstanceRegistry.cs +166 -10
  85. package/unitypkg/Editor/Infrastructure/UnityCliOperationCoordinator.cs +1 -0
  86. package/unitypkg/Editor/UnityCliBridge.cs +5 -1
  87. package/unitypkg/Tests/Editor/UnityCliInstanceCleanupTests.cs +232 -0
  88. package/unitypkg/Tests/Editor/UnityCliInstancePublicationRetryTests.cs +143 -0
  89. package/unitypkg/UnityCli.Editor.asmdef +1 -5
  90. package/unitypkg/package.json +1 -1
  91. package/unitypkg/Editor/Roslyn/LICENSE.txt +0 -23
  92. package/unitypkg/Editor/Roslyn/Microsoft.CodeAnalysis.CSharp.dll +0 -0
  93. package/unitypkg/Editor/Roslyn/Microsoft.CodeAnalysis.dll +0 -0
  94. package/unitypkg/Editor/Roslyn/System.Collections.Immutable.dll +0 -0
  95. 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 可用当前完整投影替换它。
@@ -635,7 +643,8 @@ async function verifyCurrentPackageBridge(projectPath, code, nextActions) {
635
643
  const { packageManifest, installMetadata } = await readInstalledPackageBridge(paths.projectPath, code, nextActions);
636
644
  const projectHash = await hashProjectPath(paths.projectPath);
637
645
  const sourcePath = packageSourcePath();
638
- const sourceHash = await hashSourcePath(sourcePath);
646
+ const canonical = await verifyPackageSourceIntegrity(sourcePath);
647
+ const sourceHash = canonical.sourceHash;
639
648
  if (installMetadata.packageVersion !== packageManifest.version || installMetadata.sourceHash !== sourceHash
640
649
  || installMetadata.projectHash !== projectHash || installMetadata.cliCompatibilityRange !== cliCompatibilityRange)
641
650
  throw new PackageManagerError(code, `${bridgePackageName} install metadata does not match the current project and bundled source.`, nextActions);
@@ -644,8 +653,75 @@ async function verifyCurrentPackageBridge(projectPath, code, nextActions) {
644
653
  throw new PackageManagerError(code, `${bridgePackageName} managed file projection does not match the bundled source.`, nextActions);
645
654
  return { paths, targetPath, installMetadataPath, fileManifestPath, packageManifest, installMetadata, projectHash, sourceHash, managedFiles };
646
655
  }
656
+ /** 只读检查 Bridge 投影,供 `init package --check` 区分缺失、损坏与版本漂移。 */
657
+ export async function inspectPackageBridge(projectPath) {
658
+ const paths = packagePaths(projectPath);
659
+ const sourcePath = packageSourcePath();
660
+ const canonical = await verifyPackageSourceIntegrity(sourcePath);
661
+ const assemblyInspection = await inspectPackageAssemblyConflicts(paths.projectPath);
662
+ const targetPath = join(paths.projectPath, "Packages", bridgePackageName);
663
+ if (assemblyInspection.status === "conflict") {
664
+ return { status: "conflict", projectPath: paths.projectPath, bridgePath: targetPath, sourceHash: canonical.sourceHash, reasonCode: "PACKAGE_ASSEMBLY_CONFLICT", message: `${bridgePackageName} is blocked by a protected assembly name from another Package.`, conflicts: assemblyInspection.conflicts };
665
+ }
666
+ if (!await directoryExists(targetPath)) {
667
+ return { status: "missing", projectPath: paths.projectPath, bridgePath: targetPath, sourceHash: canonical.sourceHash, reasonCode: "PACKAGE_NOT_INSTALLED", message: `${bridgePackageName} is not installed.` };
668
+ }
669
+ const installMetadataPath = join(paths.projectPath, "Packages", installMetadataFileName);
670
+ const fileManifestPath = join(targetPath, fileManifestFileName);
671
+ let packageManifest;
672
+ let installMetadata;
673
+ try {
674
+ packageManifest = await readPackageManifest(targetPath);
675
+ installMetadata = await readOptionalInstallMetadata(installMetadataPath);
676
+ }
677
+ catch (error) {
678
+ return { status: "damaged", projectPath: paths.projectPath, bridgePath: targetPath, sourceHash: canonical.sourceHash, reasonCode: "PACKAGE_INSTALL_DAMAGED", message: error instanceof Error ? error.message : String(error) };
679
+ }
680
+ if (!installMetadata || installMetadata.packageName !== bridgePackageName || installMetadata.installMode !== "copy") {
681
+ return { status: "damaged", projectPath: paths.projectPath, bridgePath: targetPath, sourceHash: canonical.sourceHash, reasonCode: "PACKAGE_INSTALL_DAMAGED", message: `${bridgePackageName} install metadata is missing or does not describe a managed copy.` };
682
+ }
683
+ const projectHash = await hashProjectPath(paths.projectPath);
684
+ if (packageManifest.version !== canonical.packageManifest.version || installMetadata.packageVersion !== canonical.packageManifest.version
685
+ || installMetadata.sourceHash !== canonical.sourceHash || installMetadata.cliCompatibilityRange !== cliCompatibilityRange) {
686
+ return { status: "drifted", projectPath: paths.projectPath, bridgePath: targetPath, sourceHash: canonical.sourceHash, reasonCode: "PACKAGE_INSTALL_DRIFTED", message: `${bridgePackageName} version or source hash differs from the bundled canonical package.` };
687
+ }
688
+ if (installMetadata.projectHash !== projectHash) {
689
+ return { status: "damaged", projectPath: paths.projectPath, bridgePath: targetPath, sourceHash: canonical.sourceHash, reasonCode: "PACKAGE_INSTALL_DAMAGED", message: `${bridgePackageName} install metadata belongs to a different canonical project.` };
690
+ }
691
+ try {
692
+ await readVerifiedManagedFileList(fileManifestPath, installMetadata.managedFilesSha256);
693
+ if (!await managedPackageFilesMatch(sourcePath, targetPath, fileManifestPath)) {
694
+ return { status: "damaged", projectPath: paths.projectPath, bridgePath: targetPath, sourceHash: canonical.sourceHash, reasonCode: "PACKAGE_INSTALL_DAMAGED", message: `${bridgePackageName} managed files are missing or do not match canonical bytes.` };
695
+ }
696
+ }
697
+ catch (error) {
698
+ return { status: "damaged", projectPath: paths.projectPath, bridgePath: targetPath, sourceHash: canonical.sourceHash, reasonCode: "PACKAGE_INSTALL_DAMAGED", message: error instanceof Error ? error.message : String(error) };
699
+ }
700
+ return {
701
+ status: "installed",
702
+ projectPath: paths.projectPath,
703
+ bridgePath: targetPath,
704
+ sourceHash: canonical.sourceHash,
705
+ install: {
706
+ manifestPath: paths.manifestPath,
707
+ bridgePath: targetPath,
708
+ dependencyName: bridgePackageName,
709
+ packageVersion: canonical.packageManifest.version,
710
+ sourceHash: canonical.sourceHash,
711
+ projectHash,
712
+ installMode: "copy",
713
+ installTime: installMetadata.installTime,
714
+ cliCompatibilityRange,
715
+ installMetadataPath,
716
+ fileManifestPath,
717
+ },
718
+ };
719
+ }
647
720
  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."]);
721
+ const assemblyInspection = await inspectPackageAssemblyConflicts(projectPath);
722
+ if (assemblyInspection.status === "conflict")
723
+ throw packageAssemblyConflictError(assemblyInspection);
724
+ const verified = await verifyCurrentPackageBridge(projectPath, "INIT_CHECK_FAILED", ["Run unitycli init package --project <UnityProject> to restore the bundled package projection."]);
649
725
  const { paths, targetPath, installMetadataPath, fileManifestPath, packageManifest, installMetadata, projectHash, sourceHash } = verified;
650
726
  return {
651
727
  manifestPath: paths.manifestPath,
@@ -667,7 +743,7 @@ export async function removePackageBridge(projectPath, options = {}) {
667
743
  return withProjectFileMutationLease(resolvedProject, "package-bridge-publish", () => removePackageBridgeLocked(resolvedProject, options));
668
744
  }
669
745
  async function removePackageBridgeLocked(projectPath, options = {}) {
670
- const verificationActions = ["Run unitycli init --project <path> to restore a fully verified bundled package before removal."];
746
+ const verificationActions = ["Run unitycli init package --project <UnityProject> to restore a fully verified bundled package before removal."];
671
747
  const verified = await verifyCurrentPackageBridge(projectPath, "BRIDGE_PACKAGE_REMOVE_UNVERIFIED", verificationActions);
672
748
  const { paths, targetPath, installMetadataPath, fileManifestPath, packageManifest, installMetadata, managedFiles } = verified;
673
749
  const plannedEffects = [
@@ -1044,15 +1120,172 @@ export async function searchRegistry(registry, query) {
1044
1120
  export class PackageManagerError extends Error {
1045
1121
  code;
1046
1122
  nextActions;
1047
- constructor(code, message, nextActions = []) {
1123
+ details;
1124
+ constructor(code, message, nextActions = [], details) {
1048
1125
  super(message);
1049
1126
  this.code = code;
1050
1127
  this.nextActions = nextActions;
1128
+ this.details = details;
1051
1129
  }
1052
1130
  }
1053
1131
  function packageSourcePath() {
1054
1132
  return resolve(fileURLToPath(new URL("../unitypkg", import.meta.url)));
1055
1133
  }
1134
+ function packageFilesContractPath() {
1135
+ return resolve(fileURLToPath(new URL("../contracts/package-files.json", import.meta.url)));
1136
+ }
1137
+ function packageAssemblyClosureContractPath() {
1138
+ return resolve(fileURLToPath(new URL("../contracts/package-assembly-closure.json", import.meta.url)));
1139
+ }
1140
+ function packageSourceIntegrityError(detail) {
1141
+ 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."]);
1142
+ }
1143
+ async function readPackageAssemblyConflictPolicy() {
1144
+ try {
1145
+ const contract = JSON.parse(stripJsonBom(await readFile(packageAssemblyClosureContractPath(), "utf8")));
1146
+ const policy = contract.conflictPolicy;
1147
+ if (contract.schemaVersion !== 1 || contract.packageName !== bridgePackageName || !policy
1148
+ || !Array.isArray(policy.protectedAssemblyFileNames) || policy.protectedAssemblyFileNames.length === 0
1149
+ || !policy.protectedAssemblyFileNames.every((item) => typeof item === "string" && item.toLowerCase().endsWith(".dll"))
1150
+ || !Array.isArray(policy.allowedPackageProviders) || !policy.allowedPackageProviders.every((item) => typeof item === "string" && item.length > 0)
1151
+ || !Number.isSafeInteger(policy.maxDirectories) || policy.maxDirectories <= 0
1152
+ || !Number.isSafeInteger(policy.maxFiles) || policy.maxFiles <= 0
1153
+ || !Number.isSafeInteger(policy.maxConflicts) || policy.maxConflicts <= 0) {
1154
+ throw new Error("assembly conflict policy schema is invalid");
1155
+ }
1156
+ return policy;
1157
+ }
1158
+ catch (error) {
1159
+ throw packageSourceIntegrityError(error instanceof Error ? error.message : String(error));
1160
+ }
1161
+ }
1162
+ function packageCacheProvider(directoryName) {
1163
+ const versionSeparator = directoryName.lastIndexOf("@");
1164
+ return versionSeparator > 0 ? directoryName.slice(0, versionSeparator) : directoryName;
1165
+ }
1166
+ function packageAssemblyScanError(code, message, details) {
1167
+ return new PackageManagerError(code, message, ["Inspect the reported package path, then remove or update the conflicting package before retrying init."], details);
1168
+ }
1169
+ /** 只读扫描项目 Package roots 中会遮蔽 UnityCLI 显式预编译引用的同名程序集。 */
1170
+ export async function inspectPackageAssemblyConflicts(projectPath) {
1171
+ const projectRoot = packagePaths(projectPath).projectPath;
1172
+ const policy = await readPackageAssemblyConflictPolicy();
1173
+ const protectedNames = new Set(policy.protectedAssemblyFileNames.map((item) => item.toLowerCase()));
1174
+ const allowedProviders = new Set(policy.allowedPackageProviders.map((item) => item.toLowerCase()));
1175
+ const conflicts = [];
1176
+ const allowedMatches = [];
1177
+ let scannedDirectories = 0;
1178
+ let scannedFiles = 0;
1179
+ const queue = [];
1180
+ const addPackageRoots = async (root, source) => {
1181
+ let entries;
1182
+ try {
1183
+ entries = await readdir(root, { withFileTypes: true });
1184
+ }
1185
+ catch (error) {
1186
+ const code = error.code;
1187
+ if (code === "ENOENT" || code === "ENOTDIR")
1188
+ return;
1189
+ throw error;
1190
+ }
1191
+ for (const entry of entries.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0)) {
1192
+ if (entry.isSymbolicLink())
1193
+ 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("/")) });
1194
+ if (!entry.isDirectory())
1195
+ continue;
1196
+ const provider = source === "Packages" ? entry.name : packageCacheProvider(entry.name);
1197
+ if (provider.toLowerCase() === bridgePackageName)
1198
+ continue;
1199
+ queue.push({ directory: join(root, entry.name), provider, source });
1200
+ }
1201
+ };
1202
+ await addPackageRoots(join(projectRoot, "Packages"), "Packages");
1203
+ await addPackageRoots(join(projectRoot, "Library", "PackageCache"), "PackageCache");
1204
+ for (let index = 0; index < queue.length; index += 1) {
1205
+ const current = queue[index];
1206
+ scannedDirectories += 1;
1207
+ if (scannedDirectories > policy.maxDirectories)
1208
+ throw packageAssemblyScanError("PACKAGE_ASSEMBLY_SCAN_LIMIT", "Package assembly scan exceeded its directory limit.", { scannedDirectories, scannedFiles });
1209
+ const entries = (await readdir(current.directory, { withFileTypes: true })).sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
1210
+ for (const entry of entries) {
1211
+ const absolute = join(current.directory, entry.name);
1212
+ const relativePath = posix.normalize(relative(projectRoot, absolute).split("\\").join("/"));
1213
+ if (entry.isSymbolicLink())
1214
+ throw packageAssemblyScanError("PACKAGE_ASSEMBLY_SCAN_UNSAFE", "Package assembly scan found a linked entry.", { path: relativePath, provider: current.provider });
1215
+ if (entry.isDirectory()) {
1216
+ queue.push({ ...current, directory: absolute });
1217
+ continue;
1218
+ }
1219
+ if (!entry.isFile())
1220
+ continue;
1221
+ scannedFiles += 1;
1222
+ if (scannedFiles > policy.maxFiles)
1223
+ throw packageAssemblyScanError("PACKAGE_ASSEMBLY_SCAN_LIMIT", "Package assembly scan exceeded its file limit.", { scannedDirectories, scannedFiles });
1224
+ if (!protectedNames.has(entry.name.toLowerCase()))
1225
+ continue;
1226
+ const match = {
1227
+ assemblyFileName: entry.name,
1228
+ provider: current.provider,
1229
+ source: current.source,
1230
+ relativePath,
1231
+ sha256: createHash("sha256").update(await readFile(absolute)).digest("hex"),
1232
+ };
1233
+ if (allowedProviders.has(current.provider.toLowerCase()))
1234
+ allowedMatches.push(match);
1235
+ else {
1236
+ conflicts.push(match);
1237
+ if (conflicts.length >= policy.maxConflicts)
1238
+ throw packageAssemblyScanError("PACKAGE_ASSEMBLY_SCAN_LIMIT", "Package assembly scan reached its conflict limit.", { scannedDirectories, scannedFiles, conflicts });
1239
+ }
1240
+ }
1241
+ }
1242
+ return { status: conflicts.length ? "conflict" : "clear", conflicts, allowedMatches, scannedDirectories, scannedFiles };
1243
+ }
1244
+ function packageAssemblyConflictError(inspection) {
1245
+ 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 });
1246
+ }
1247
+ /** 安装源必须逐文件匹配 npm artifact 内固定清单;任何异常都在目标 effect 前 fail closed。 */
1248
+ export async function verifyPackageSourceIntegrity(sourcePath, contractPath = packageFilesContractPath()) {
1249
+ try {
1250
+ const contract = JSON.parse(stripJsonBom(await readFile(contractPath, "utf8")));
1251
+ const packageManifest = await readPackageManifest(sourcePath);
1252
+ if (contract.schemaVersion !== 1 || contract.packageName !== bridgePackageName || contract.packageName !== packageManifest.name
1253
+ || contract.packageVersion !== packageManifest.version || !isSha256(contract.sourceHash) || !Array.isArray(contract.files) || contract.files.length === 0) {
1254
+ throw packageSourceIntegrityError("canonical manifest identity or schema is invalid");
1255
+ }
1256
+ const expectedFiles = await listManagedSourceFiles(sourcePath);
1257
+ const identities = new Set();
1258
+ const manifestFiles = [];
1259
+ for (const rawEntry of contract.files) {
1260
+ if (!rawEntry || typeof rawEntry !== "object")
1261
+ throw packageSourceIntegrityError("canonical manifest contains a non-object file entry");
1262
+ const entry = rawEntry;
1263
+ assertManagedRelativeFilePath(entry.path, contractPath);
1264
+ if (!Number.isSafeInteger(entry.size) || entry.size < 0 || !isSha256(entry.sha256))
1265
+ throw packageSourceIntegrityError(`canonical manifest entry is invalid: ${entry.path}`);
1266
+ const identity = entry.path.toLowerCase();
1267
+ if (identities.has(identity))
1268
+ throw packageSourceIntegrityError(`canonical manifest contains a duplicate path: ${entry.path}`);
1269
+ identities.add(identity);
1270
+ manifestFiles.push(entry.path);
1271
+ const content = await readFile(join(sourcePath, entry.path));
1272
+ const sha256 = createHash("sha256").update(content).digest("hex");
1273
+ if (content.length !== entry.size || sha256 !== entry.sha256)
1274
+ throw packageSourceIntegrityError(`canonical file bytes do not match: ${entry.path}`);
1275
+ }
1276
+ if (manifestFiles.length !== expectedFiles.length || manifestFiles.some((file, index) => file !== expectedFiles[index]))
1277
+ throw packageSourceIntegrityError("canonical file set does not match the bundled package directory");
1278
+ const sourceHash = await hashSourcePath(sourcePath);
1279
+ if (sourceHash !== contract.sourceHash)
1280
+ throw packageSourceIntegrityError("canonical package tree hash does not match");
1281
+ return { packageManifest, sourceHash, files: manifestFiles };
1282
+ }
1283
+ catch (error) {
1284
+ if (error instanceof PackageManagerError && error.code === "PACKAGE_SOURCE_INTEGRITY_FAILED")
1285
+ throw error;
1286
+ throw packageSourceIntegrityError(error instanceof Error ? error.message : String(error));
1287
+ }
1288
+ }
1056
1289
  /** 读取并确认项目内 Bridge 包是 init 托管的 copy-mode 安装。 */
1057
1290
  async function readInstalledPackageBridge(projectPath, code, nextActions) {
1058
1291
  const targetPath = join(projectPath, "Packages", bridgePackageName);
@@ -1153,7 +1386,7 @@ async function readInstallMetadata(path) {
1153
1386
  try {
1154
1387
  const metadata = JSON.parse(stripJsonBom(await readFile(path, "utf8")));
1155
1388
  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."]);
1389
+ throw new PackageManagerError("INIT_CHECK_FAILED", `Install metadata is invalid: ${path}`, ["Run unitycli init package --project <UnityProject> to refresh install metadata."]);
1157
1390
  }
1158
1391
  return metadata;
1159
1392
  }
@@ -1163,10 +1396,10 @@ async function readInstallMetadata(path) {
1163
1396
  }
1164
1397
  const nodeError = error;
1165
1398
  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."]);
1399
+ throw new PackageManagerError("INIT_CHECK_FAILED", `Install metadata not found: ${path}`, ["Run unitycli init package --project <UnityProject> to create install metadata."]);
1167
1400
  }
1168
1401
  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."]);
1402
+ throw new PackageManagerError("INIT_CHECK_FAILED", `Install metadata is invalid JSON: ${path}`, ["Run unitycli init package --project <UnityProject> to refresh install metadata."]);
1170
1403
  }
1171
1404
  throw error;
1172
1405
  }
@@ -1387,12 +1620,18 @@ async function managedPackageFilesMatch(sourcePath, targetPath, fileManifestPath
1387
1620
  async function listManagedSourceFiles(sourcePath) {
1388
1621
  const files = [];
1389
1622
  const visit = async (dir) => {
1390
- for (const entry of await readdir(dir, { withFileTypes: true })) {
1623
+ const entries = (await readdir(dir, { withFileTypes: true })).sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
1624
+ for (const entry of entries) {
1391
1625
  const fullPath = join(dir, entry.name);
1626
+ const relPath = posix.normalize(relative(sourcePath, fullPath).split("\\").join("/"));
1627
+ if (entry.isSymbolicLink())
1628
+ throw packageSourceIntegrityError(`canonical package source contains a link: ${relPath}`);
1392
1629
  if (entry.isDirectory())
1393
1630
  await visit(fullPath);
1394
1631
  else if (entry.isFile())
1395
- files.push(posix.normalize(relative(sourcePath, fullPath).split("\\").join("/")));
1632
+ files.push(relPath);
1633
+ else
1634
+ throw packageSourceIntegrityError(`canonical package source contains an unsupported entry: ${relPath}`);
1396
1635
  }
1397
1636
  };
1398
1637
  await visit(sourcePath);