@aefree/pi-unity 0.9.0 → 0.9.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/CHANGELOG.md +190 -173
- package/README.md +234 -197
- package/index.ts +1724 -1724
- package/package.json +75 -75
- package/skills/unity-batchmode-tests/SKILL.md +145 -145
- package/skills/unity-interactive-playmode-authoring/SKILL.md +91 -91
- package/skills/unity-pipeline-workflows/SKILL.md +52 -52
- package/src/unity-artifact-profile.ts +110 -110
- package/src/unity-batchmode.ts +355 -355
- package/src/unity-cli.ts +635 -635
- package/src/unity-file-discovery-filter.ts +89 -89
- package/src/unity-pipeline.ts +487 -487
|
@@ -1,89 +1,89 @@
|
|
|
1
|
-
import { access, readdir } from "node:fs/promises";
|
|
2
|
-
import { fileURLToPath } from "node:url";
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
import type {
|
|
5
|
-
FileDiscoveryExecutionContextV1,
|
|
6
|
-
FileDiscoveryFilterRequestV1,
|
|
7
|
-
FileDiscoveryFilterResultV1,
|
|
8
|
-
FileDiscoveryFilterV1,
|
|
9
|
-
} from "@aefree/pi-file-discovery/contracts/v1";
|
|
10
|
-
|
|
11
|
-
export const UNITY_FILE_DISCOVERY_FILTER_ID_V1 = "unity.generated-directories-filter.v1" as const;
|
|
12
|
-
export const UNITY_BROAD_GENERATED_DIRECTORIES_APPLIED_CODE = "unity_broad_generated_directories_applied" as const;
|
|
13
|
-
export const UNITY_EXACT_GENERATED_ROOT_BYPASSED_CODE = "unity_exact_generated_root_bypassed" as const;
|
|
14
|
-
export const UNITY_GENERATED_DIRECTORIES = Object.freeze(["Library", "Temp", "Logs", "obj", "Build", "Builds", "UserSettings", ".vs"] as const);
|
|
15
|
-
|
|
16
|
-
export function createUnityFileDiscoveryFilterV1(): FileDiscoveryFilterV1 {
|
|
17
|
-
return Object.freeze({
|
|
18
|
-
contractVersion: 1,
|
|
19
|
-
id: UNITY_FILE_DISCOVERY_FILTER_ID_V1,
|
|
20
|
-
kind: "file-discovery-filter",
|
|
21
|
-
owner: Object.freeze({ packageName: "@aefree/pi-unity", packageVersion: "0.8.3", packageRoot: path.resolve(fileURLToPath(new URL("..", import.meta.url))), registeredBy: "index.ts" }),
|
|
22
|
-
async evaluate(context, request) { return await evaluateUnityFileDiscoveryFilterV1(context, request); },
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export async function evaluateUnityFileDiscoveryFilterV1(
|
|
27
|
-
context: FileDiscoveryExecutionContextV1,
|
|
28
|
-
request: FileDiscoveryFilterRequestV1,
|
|
29
|
-
): Promise<FileDiscoveryFilterResultV1> {
|
|
30
|
-
if (context.signal !== request.signal) return { outcome: "error", code: "unity_signal_mismatch", retryable: false };
|
|
31
|
-
if (request.signal.aborted) return { outcome: "unavailable", code: "aborted", retryable: true };
|
|
32
|
-
const unityRoots = await discoverUnityRoots(request.workspaceRoot, request.roots, request.signal);
|
|
33
|
-
if (unityRoots.length === 0) return { outcome: "not_applicable" };
|
|
34
|
-
const roots = request.roots.map((searchRoot) => {
|
|
35
|
-
const absoluteSearchRoot = path.resolve(request.workspaceRoot, searchRoot);
|
|
36
|
-
const globs = new Set<string>();
|
|
37
|
-
let generatedRoot = false;
|
|
38
|
-
for (const unityRoot of unityRoots) {
|
|
39
|
-
if (isInside(unityRoot, absoluteSearchRoot)) {
|
|
40
|
-
const first = path.relative(unityRoot, absoluteSearchRoot).split(path.sep).filter(Boolean)[0];
|
|
41
|
-
if (first && UNITY_GENERATED_DIRECTORIES.some((entry) => entry.toLowerCase() === first.toLowerCase())) generatedRoot = true;
|
|
42
|
-
}
|
|
43
|
-
if (!isInside(absoluteSearchRoot, unityRoot)) continue;
|
|
44
|
-
const prefix = normalize(path.relative(absoluteSearchRoot, unityRoot));
|
|
45
|
-
for (const directory of UNITY_GENERATED_DIRECTORIES) globs.add(`!${prefix ? `${prefix}/` : ""}${directory}/**`);
|
|
46
|
-
}
|
|
47
|
-
// An exact root inside a generated directory is deliberate research intent.
|
|
48
|
-
return Object.freeze({
|
|
49
|
-
root: searchRoot,
|
|
50
|
-
filterDecision: generatedRoot ? "bypassed" : "applied",
|
|
51
|
-
decisionCode: generatedRoot ? UNITY_EXACT_GENERATED_ROOT_BYPASSED_CODE : UNITY_BROAD_GENERATED_DIRECTORIES_APPLIED_CODE,
|
|
52
|
-
...(generatedRoot ? {} : { excludeGlobs: Object.freeze([...globs].sort()) }),
|
|
53
|
-
disclosures: Object.freeze([generatedRoot
|
|
54
|
-
? "Unity generated/cache/output root filter bypassed for the explicit root; it is searched."
|
|
55
|
-
: "Unity broad-root generated-directory filter applied; excludes Library, Temp, Logs, obj, Build, Builds, UserSettings, .vs."]),
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
return Object.freeze({ outcome: "applied", roots: Object.freeze(roots) });
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async function discoverUnityRoots(workspaceRoot: string, searchRoots: readonly string[], signal: AbortSignal): Promise<string[]> {
|
|
62
|
-
const workspace = path.resolve(workspaceRoot);
|
|
63
|
-
const found = new Set<string>();
|
|
64
|
-
for (const rawRoot of searchRoots) {
|
|
65
|
-
if (signal.aborted) return [];
|
|
66
|
-
const root = path.resolve(workspace, rawRoot);
|
|
67
|
-
for (let current = root; isInside(workspace, current); current = path.dirname(current)) {
|
|
68
|
-
if (await isUnityProject(current)) { found.add(current); break; }
|
|
69
|
-
if (current === workspace) break;
|
|
70
|
-
}
|
|
71
|
-
if (!isInside(workspace, root)) continue;
|
|
72
|
-
await discoverBelow(root, found, signal, 0);
|
|
73
|
-
}
|
|
74
|
-
return [...found].sort((left, right) => normalize(left).localeCompare(normalize(right)));
|
|
75
|
-
}
|
|
76
|
-
async function discoverBelow(root: string, found: Set<string>, signal: AbortSignal, depth: number): Promise<void> {
|
|
77
|
-
if (depth > 8 || signal.aborted || found.size >= 64) return;
|
|
78
|
-
if (await isUnityProject(root)) { found.add(root); return; }
|
|
79
|
-
let entries;
|
|
80
|
-
try { entries = await readdir(root, { withFileTypes: true }); } catch { return; }
|
|
81
|
-
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
82
|
-
if (signal.aborted || found.size >= 64) return;
|
|
83
|
-
if (!entry.isDirectory() || entry.isSymbolicLink() || UNITY_GENERATED_DIRECTORIES.some((name) => name.toLowerCase() === entry.name.toLowerCase()) || entry.name === ".git" || entry.name === "node_modules") continue;
|
|
84
|
-
await discoverBelow(path.join(root, entry.name), found, signal, depth + 1);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
async function isUnityProject(root: string): Promise<boolean> { try { await access(path.join(root, "ProjectSettings", "ProjectVersion.txt")); await access(path.join(root, "Assets")); return true; } catch { return false; } }
|
|
88
|
-
function normalize(value: string): string { return value.replaceAll("\\", "/"); }
|
|
89
|
-
function isInside(parent: string, child: string): boolean { const relative = path.relative(path.resolve(parent), path.resolve(child)); return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)); }
|
|
1
|
+
import { access, readdir } from "node:fs/promises";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import type {
|
|
5
|
+
FileDiscoveryExecutionContextV1,
|
|
6
|
+
FileDiscoveryFilterRequestV1,
|
|
7
|
+
FileDiscoveryFilterResultV1,
|
|
8
|
+
FileDiscoveryFilterV1,
|
|
9
|
+
} from "@aefree/pi-file-discovery/contracts/v1";
|
|
10
|
+
|
|
11
|
+
export const UNITY_FILE_DISCOVERY_FILTER_ID_V1 = "unity.generated-directories-filter.v1" as const;
|
|
12
|
+
export const UNITY_BROAD_GENERATED_DIRECTORIES_APPLIED_CODE = "unity_broad_generated_directories_applied" as const;
|
|
13
|
+
export const UNITY_EXACT_GENERATED_ROOT_BYPASSED_CODE = "unity_exact_generated_root_bypassed" as const;
|
|
14
|
+
export const UNITY_GENERATED_DIRECTORIES = Object.freeze(["Library", "Temp", "Logs", "obj", "Build", "Builds", "UserSettings", ".vs"] as const);
|
|
15
|
+
|
|
16
|
+
export function createUnityFileDiscoveryFilterV1(): FileDiscoveryFilterV1 {
|
|
17
|
+
return Object.freeze({
|
|
18
|
+
contractVersion: 1,
|
|
19
|
+
id: UNITY_FILE_DISCOVERY_FILTER_ID_V1,
|
|
20
|
+
kind: "file-discovery-filter",
|
|
21
|
+
owner: Object.freeze({ packageName: "@aefree/pi-unity", packageVersion: "0.8.3", packageRoot: path.resolve(fileURLToPath(new URL("..", import.meta.url))), registeredBy: "index.ts" }),
|
|
22
|
+
async evaluate(context, request) { return await evaluateUnityFileDiscoveryFilterV1(context, request); },
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function evaluateUnityFileDiscoveryFilterV1(
|
|
27
|
+
context: FileDiscoveryExecutionContextV1,
|
|
28
|
+
request: FileDiscoveryFilterRequestV1,
|
|
29
|
+
): Promise<FileDiscoveryFilterResultV1> {
|
|
30
|
+
if (context.signal !== request.signal) return { outcome: "error", code: "unity_signal_mismatch", retryable: false };
|
|
31
|
+
if (request.signal.aborted) return { outcome: "unavailable", code: "aborted", retryable: true };
|
|
32
|
+
const unityRoots = await discoverUnityRoots(request.workspaceRoot, request.roots, request.signal);
|
|
33
|
+
if (unityRoots.length === 0) return { outcome: "not_applicable" };
|
|
34
|
+
const roots = request.roots.map((searchRoot) => {
|
|
35
|
+
const absoluteSearchRoot = path.resolve(request.workspaceRoot, searchRoot);
|
|
36
|
+
const globs = new Set<string>();
|
|
37
|
+
let generatedRoot = false;
|
|
38
|
+
for (const unityRoot of unityRoots) {
|
|
39
|
+
if (isInside(unityRoot, absoluteSearchRoot)) {
|
|
40
|
+
const first = path.relative(unityRoot, absoluteSearchRoot).split(path.sep).filter(Boolean)[0];
|
|
41
|
+
if (first && UNITY_GENERATED_DIRECTORIES.some((entry) => entry.toLowerCase() === first.toLowerCase())) generatedRoot = true;
|
|
42
|
+
}
|
|
43
|
+
if (!isInside(absoluteSearchRoot, unityRoot)) continue;
|
|
44
|
+
const prefix = normalize(path.relative(absoluteSearchRoot, unityRoot));
|
|
45
|
+
for (const directory of UNITY_GENERATED_DIRECTORIES) globs.add(`!${prefix ? `${prefix}/` : ""}${directory}/**`);
|
|
46
|
+
}
|
|
47
|
+
// An exact root inside a generated directory is deliberate research intent.
|
|
48
|
+
return Object.freeze({
|
|
49
|
+
root: searchRoot,
|
|
50
|
+
filterDecision: generatedRoot ? "bypassed" : "applied",
|
|
51
|
+
decisionCode: generatedRoot ? UNITY_EXACT_GENERATED_ROOT_BYPASSED_CODE : UNITY_BROAD_GENERATED_DIRECTORIES_APPLIED_CODE,
|
|
52
|
+
...(generatedRoot ? {} : { excludeGlobs: Object.freeze([...globs].sort()) }),
|
|
53
|
+
disclosures: Object.freeze([generatedRoot
|
|
54
|
+
? "Unity generated/cache/output root filter bypassed for the explicit root; it is searched."
|
|
55
|
+
: "Unity broad-root generated-directory filter applied; excludes Library, Temp, Logs, obj, Build, Builds, UserSettings, .vs."]),
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
return Object.freeze({ outcome: "applied", roots: Object.freeze(roots) });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function discoverUnityRoots(workspaceRoot: string, searchRoots: readonly string[], signal: AbortSignal): Promise<string[]> {
|
|
62
|
+
const workspace = path.resolve(workspaceRoot);
|
|
63
|
+
const found = new Set<string>();
|
|
64
|
+
for (const rawRoot of searchRoots) {
|
|
65
|
+
if (signal.aborted) return [];
|
|
66
|
+
const root = path.resolve(workspace, rawRoot);
|
|
67
|
+
for (let current = root; isInside(workspace, current); current = path.dirname(current)) {
|
|
68
|
+
if (await isUnityProject(current)) { found.add(current); break; }
|
|
69
|
+
if (current === workspace) break;
|
|
70
|
+
}
|
|
71
|
+
if (!isInside(workspace, root)) continue;
|
|
72
|
+
await discoverBelow(root, found, signal, 0);
|
|
73
|
+
}
|
|
74
|
+
return [...found].sort((left, right) => normalize(left).localeCompare(normalize(right)));
|
|
75
|
+
}
|
|
76
|
+
async function discoverBelow(root: string, found: Set<string>, signal: AbortSignal, depth: number): Promise<void> {
|
|
77
|
+
if (depth > 8 || signal.aborted || found.size >= 64) return;
|
|
78
|
+
if (await isUnityProject(root)) { found.add(root); return; }
|
|
79
|
+
let entries;
|
|
80
|
+
try { entries = await readdir(root, { withFileTypes: true }); } catch { return; }
|
|
81
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
82
|
+
if (signal.aborted || found.size >= 64) return;
|
|
83
|
+
if (!entry.isDirectory() || entry.isSymbolicLink() || UNITY_GENERATED_DIRECTORIES.some((name) => name.toLowerCase() === entry.name.toLowerCase()) || entry.name === ".git" || entry.name === "node_modules") continue;
|
|
84
|
+
await discoverBelow(path.join(root, entry.name), found, signal, depth + 1);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function isUnityProject(root: string): Promise<boolean> { try { await access(path.join(root, "ProjectSettings", "ProjectVersion.txt")); await access(path.join(root, "Assets")); return true; } catch { return false; } }
|
|
88
|
+
function normalize(value: string): string { return value.replaceAll("\\", "/"); }
|
|
89
|
+
function isInside(parent: string, child: string): boolean { const relative = path.relative(path.resolve(parent), path.resolve(child)); return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)); }
|