@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.
- package/README.md +111 -299
- package/contracts/init-contract.json +64 -0
- package/contracts/instances-contract.json +49 -0
- package/contracts/package-assembly-closure.json +33 -0
- package/contracts/package-files.json +1953 -0
- package/dist/bridge/session-supervisor.d.ts +2 -0
- package/dist/bridge/session-supervisor.js +52 -19
- package/dist/bridge/session-supervisor.js.map +1 -1
- package/dist/cli-commands/command.js +33 -15
- package/dist/cli-commands/command.js.map +1 -1
- package/dist/cli-commands/init-discovery.d.ts +11 -0
- package/dist/cli-commands/init-discovery.js +185 -0
- package/dist/cli-commands/init-discovery.js.map +1 -0
- package/dist/cli-commands/init-help.d.ts +3 -0
- package/dist/cli-commands/init-help.js +41 -0
- package/dist/cli-commands/init-help.js.map +1 -0
- package/dist/cli-commands/init-skill.d.ts +10 -0
- package/dist/cli-commands/init-skill.js +171 -0
- package/dist/cli-commands/init-skill.js.map +1 -0
- package/dist/cli-commands/instances.d.ts +4 -0
- package/dist/cli-commands/instances.js +90 -0
- package/dist/cli-commands/instances.js.map +1 -0
- package/dist/cli-commands/package.d.ts +2 -2
- package/dist/cli-commands/package.js +97 -130
- package/dist/cli-commands/package.js.map +1 -1
- package/dist/cli-commands/skill-installer.d.ts +29 -13
- package/dist/cli-commands/skill-installer.js +127 -88
- package/dist/cli-commands/skill-installer.js.map +1 -1
- package/dist/cli-commands/types.d.ts +4 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +46 -37
- package/dist/cli.js.map +1 -1
- package/dist/contracts/command-contract.js +1 -0
- package/dist/contracts/command-contract.js.map +1 -1
- package/dist/doctor.js +1 -1
- package/dist/doctor.js.map +1 -1
- package/dist/instances.d.ts +1 -1
- package/dist/instances.js +63 -38
- package/dist/instances.js.map +1 -1
- package/dist/output.d.ts +4 -4
- package/dist/output.js +3 -4
- package/dist/output.js.map +1 -1
- package/dist/package-manager.d.ts +47 -2
- package/dist/package-manager.js +267 -16
- package/dist/package-manager.js.map +1 -1
- package/dist/tools/.generated-schemas.js +2 -2
- package/dist/tools/.generated-schemas.js.map +1 -1
- package/dist/tools/commands.js +11 -4
- package/dist/tools/commands.js.map +1 -1
- package/dist/tools/dispatcher.js +1 -1
- package/dist/tools/dispatcher.js.map +1 -1
- package/dist/tools/tools/group-list.d.ts +4 -8
- package/dist/tools/tools/group-list.js +12 -13
- package/dist/tools/tools/group-list.js.map +1 -1
- package/dist/tools/tools/pagination.d.ts +17 -0
- package/dist/tools/tools/pagination.js +28 -0
- package/dist/tools/tools/pagination.js.map +1 -0
- package/dist/tools/tools/search.d.ts +3 -5
- package/dist/tools/tools/search.js +12 -12
- package/dist/tools/tools/search.js.map +1 -1
- package/dist/unity-project-processes.d.ts +7 -0
- package/dist/unity-project-processes.js +54 -0
- package/dist/unity-project-processes.js.map +1 -1
- package/docs/README.md +7 -4
- package/docs/architecture.md +103 -0
- package/docs/availability-differences.md +1 -1
- package/docs/command-execution.md +6 -1
- package/docs/development.md +91 -0
- package/docs/error-codes.md +21 -5
- package/docs/extensions.md +13 -13
- package/docs/target-and-instance.md +6 -4
- package/docs/tool-discovery.md +12 -10
- package/docs/tool-execution.md +3 -2
- package/package.json +10 -4
- package/schemas/init.schema.json +209 -0
- package/schemas/instances.schema.json +104 -0
- package/skills/unitycli/SKILL.md +18 -6
- package/skills/unitycli/manifest.json +2 -2
- package/skills/unitycli/references/custom-tools.md +3 -3
- package/skills/unitycli/references/operation-protocol.md +6 -4
- package/skills/unitycli/reports/output_quality_scorecard.md +1 -1
- package/skills/unitycli/reports/trust-report.md +1 -1
- package/unitypkg/Editor/Bridge/BridgeCommandRegistry.cs +1 -0
- package/unitypkg/Editor/Bridge/BridgeWorkQueue.cs +18 -6
- package/unitypkg/Editor/Infrastructure/CodeExecSupport.cs +8 -77
- package/unitypkg/Editor/Infrastructure/UnityCliInstanceCleanup.cs +295 -0
- package/unitypkg/Editor/Infrastructure/UnityCliInstanceRegistry.cs +166 -10
- package/unitypkg/Editor/Infrastructure/UnityCliOperationCoordinator.cs +1 -0
- package/unitypkg/Editor/UnityCliBridge.cs +5 -1
- package/unitypkg/Tests/Editor/UnityCliInstanceCleanupTests.cs +232 -0
- package/unitypkg/Tests/Editor/UnityCliInstancePublicationRetryTests.cs +143 -0
- package/unitypkg/UnityCli.Editor.asmdef +1 -5
- package/unitypkg/package.json +1 -1
- package/unitypkg/Editor/Roslyn/LICENSE.txt +0 -23
- package/unitypkg/Editor/Roslyn/Microsoft.CodeAnalysis.CSharp.dll +0 -0
- package/unitypkg/Editor/Roslyn/Microsoft.CodeAnalysis.dll +0 -0
- package/unitypkg/Editor/Roslyn/System.Collections.Immutable.dll +0 -0
- package/unitypkg/Editor/Roslyn/System.Reflection.Metadata.dll +0 -0
package/dist/package-manager.js
CHANGED
|
@@ -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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
const
|
|
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 =
|
|
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
|
|
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
|
|
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 <
|
|
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
|
-
|
|
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 <
|
|
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 <
|
|
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 <
|
|
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
|
-
|
|
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(
|
|
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);
|