@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.
- package/README.md +109 -300
- package/contracts/init-contract.json +57 -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 +4 -0
- package/dist/cli-commands/init-discovery.js +175 -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 +37 -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 +105 -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 +57 -137
- package/dist/cli-commands/package.js.map +1 -1
- package/dist/cli-commands/skill-installer.d.ts +14 -6
- package/dist/cli-commands/skill-installer.js +37 -38
- package/dist/cli-commands/skill-installer.js.map +1 -1
- package/dist/cli-commands/types.d.ts +1 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +44 -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 +43 -2
- package/dist/package-manager.js +255 -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/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 +5 -5
- package/docs/extensions.md +11 -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 +124 -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 可用当前完整投影替换它。
|
|
@@ -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
|
|
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
|
|
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 <
|
|
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
|
-
|
|
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 <
|
|
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 <
|
|
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 <
|
|
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
|
-
|
|
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(
|
|
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);
|