@aeonsparx/pi-unreal 0.0.0-stage → 1.1.0
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 +89 -0
- package/LICENSE +21 -0
- package/README.md +199 -2
- package/docs/internals.md +245 -0
- package/extensions/unreal/index.ts +696 -0
- package/extensions/unreal/lib/cache.ts +190 -0
- package/extensions/unreal/lib/compiledb.ts +491 -0
- package/extensions/unreal/lib/descriptors.ts +631 -0
- package/extensions/unreal/lib/engine-resolver.ts +443 -0
- package/extensions/unreal/lib/pi-tools.ts +452 -0
- package/extensions/unreal/lib/plugin-resolver.ts +305 -0
- package/extensions/unreal/lib/query.ts +272 -0
- package/extensions/unreal/lib/registry.ts +208 -0
- package/extensions/unreal/lib/scan.ts +510 -0
- package/extensions/unreal/lib/store.ts +1490 -0
- package/extensions/unreal/lib/tools.ts +1074 -0
- package/extensions/unreal/lib/ubt.ts +380 -0
- package/extensions/unreal/lib/util.ts +210 -0
- package/package.json +60 -4
- package/prompts/unreal-context.md +36 -0
- package/skills/ue-engine-resolution/SKILL.md +129 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windows registry / launcher enumeration for Unreal Engine installs.
|
|
3
|
+
*
|
|
4
|
+
* Source of truth mirrored from engine code:
|
|
5
|
+
* Engine/Source/Developer/DesktopPlatform/Private/DesktopPlatformWindows.cpp
|
|
6
|
+
* - `HKLM:\SOFTWARE\EpicGames\Unreal Engine\<version>` -> `InstalledDirectory`
|
|
7
|
+
* - `HKLM:\SOFTWARE\EpicGames\Unreal Engine` default value (INSTALLDIR)
|
|
8
|
+
* - `HKCU:\SOFTWARE\Epic Games\Unreal Engine\Builds` -> value name is the GUID
|
|
9
|
+
* association identifier, value data is the engine root (source builds)
|
|
10
|
+
* Engine/Source/Developer/DesktopPlatform/Private/DesktopPlatformBase.cpp
|
|
11
|
+
* - `%PROGRAMDATA%\UnrealEngineLauncher\LauncherInstalled.dat`
|
|
12
|
+
*
|
|
13
|
+
* Reads only. This module never writes a registry key — which matters, because the engine's
|
|
14
|
+
* own `GetEngineIdentifierFromRootDir()` falls back to `RegisterEngineInstallation()` and
|
|
15
|
+
* *writes* `HKCU\...\Builds` when it cannot map a directory. An inspection tool must not do that.
|
|
16
|
+
*
|
|
17
|
+
* `pwsh` is used instead of `reg.exe` because `reg.exe` arguments get mangled by MSYS path
|
|
18
|
+
* conversion in Git-Bash style shells on this machine (verified: `/s` becomes `S:/`, and
|
|
19
|
+
* `MSYS_NO_PATHCONV=1` does not prevent it).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { execFile } from "node:child_process";
|
|
23
|
+
import { writeFileSync } from "node:fs";
|
|
24
|
+
import { tmpdir } from "node:os";
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
|
|
27
|
+
export interface HklmEngineKey {
|
|
28
|
+
version: string;
|
|
29
|
+
installedDirectory?: string;
|
|
30
|
+
/** `(Default)` value, historically the INSTALLDIR. */
|
|
31
|
+
installDirDefault?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface HkcuBuildEntry {
|
|
35
|
+
/** Value name: the GUID association identifier written into `.uproject`. */
|
|
36
|
+
identifier: string;
|
|
37
|
+
/** Value data: engine root directory. */
|
|
38
|
+
root: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface RegistrySnapshot {
|
|
42
|
+
/** False when not Windows, `pwsh` is missing, or the registry read failed. */
|
|
43
|
+
available: boolean;
|
|
44
|
+
hklmVersions: HklmEngineKey[];
|
|
45
|
+
hkcuBuilds: HkcuBuildEntry[];
|
|
46
|
+
installDirDefault?: string;
|
|
47
|
+
launcherDat?: { appName: string; installLocation: string; buildVersion?: string }[];
|
|
48
|
+
launcherDatPath: string;
|
|
49
|
+
launcherDatExists: boolean;
|
|
50
|
+
envUnrealEngine?: string;
|
|
51
|
+
error?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type RegistryReader = () => Promise<RegistrySnapshot>;
|
|
55
|
+
|
|
56
|
+
const EMPTY: RegistrySnapshot = {
|
|
57
|
+
available: false,
|
|
58
|
+
hklmVersions: [],
|
|
59
|
+
hkcuBuilds: [],
|
|
60
|
+
launcherDatPath: "",
|
|
61
|
+
launcherDatExists: false,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const PS_SCRIPT = `
|
|
65
|
+
$ErrorActionPreference = 'SilentlyContinue'
|
|
66
|
+
$out = [ordered]@{
|
|
67
|
+
hklmVersions = @()
|
|
68
|
+
hkcuBuilds = @()
|
|
69
|
+
installDirDefault = $null
|
|
70
|
+
launcherDat = @()
|
|
71
|
+
launcherDatPath = ''
|
|
72
|
+
launcherDatExists = $false
|
|
73
|
+
envUnrealEngine = $env:UNREAL_ENGINE
|
|
74
|
+
error = $null
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
$root = 'HKLM:\\SOFTWARE\\EpicGames\\Unreal Engine'
|
|
78
|
+
foreach ($key in (Get-ChildItem -Path $root -ErrorAction SilentlyContinue)) {
|
|
79
|
+
$p = Get-ItemProperty -Path $key.PSPath -ErrorAction SilentlyContinue
|
|
80
|
+
$out.hklmVersions += [ordered]@{
|
|
81
|
+
version = $key.PSChildName
|
|
82
|
+
installedDirectory = [string]$p.'InstalledDirectory'
|
|
83
|
+
installDirDefault = [string]$p.'(Default)'
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
$base = Get-ItemProperty -Path $root -ErrorAction SilentlyContinue
|
|
87
|
+
$out.installDirDefault = [string]$base.'(Default)'
|
|
88
|
+
|
|
89
|
+
$builds = Get-ItemProperty -Path 'HKCU:\\SOFTWARE\\Epic Games\\Unreal Engine\\Builds' -ErrorAction SilentlyContinue
|
|
90
|
+
if ($builds) {
|
|
91
|
+
foreach ($prop in $builds.PSObject.Properties) {
|
|
92
|
+
if ($prop.Name -like 'PS*') { continue }
|
|
93
|
+
$out.hkcuBuilds += [ordered]@{ identifier = $prop.Name; root = [string]$prop.Value }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
$dat = Join-Path $env:PROGRAMDATA 'UnrealEngineLauncher\\LauncherInstalled.dat'
|
|
98
|
+
$out.launcherDatPath = $dat
|
|
99
|
+
$out.launcherDatExists = Test-Path -LiteralPath $dat
|
|
100
|
+
if ($out.launcherDatExists) {
|
|
101
|
+
$json = Get-Content -Raw -LiteralPath $dat | ConvertFrom-Json
|
|
102
|
+
foreach ($entry in $json.InstallationList) {
|
|
103
|
+
$out.launcherDat += [ordered]@{
|
|
104
|
+
appName = [string]$entry.AppName
|
|
105
|
+
installLocation = [string]$entry.InstallLocation
|
|
106
|
+
buildVersion = [string]$entry.BuildVersion
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
$out.error = $_.Exception.Message
|
|
112
|
+
}
|
|
113
|
+
$result = [ordered]@{
|
|
114
|
+
hklmVersions = [array]$out.hklmVersions
|
|
115
|
+
hkcuBuilds = [array]$out.hkcuBuilds
|
|
116
|
+
installDirDefault = $out.installDirDefault
|
|
117
|
+
launcherDat = [array]$out.launcherDat
|
|
118
|
+
launcherDatPath = $out.launcherDatPath
|
|
119
|
+
launcherDatExists = $out.launcherDatExists
|
|
120
|
+
envUnrealEngine = $out.envUnrealEngine
|
|
121
|
+
error = $out.error
|
|
122
|
+
}
|
|
123
|
+
$result | ConvertTo-Json -Depth 6 -Compress
|
|
124
|
+
`;
|
|
125
|
+
|
|
126
|
+
/* The arrays are cast explicitly so `ConvertTo-Json` cannot collapse a one-element array into a
|
|
127
|
+
bare object (PowerShell's habit); `toArray` below normalizes them again on the TypeScript side. */
|
|
128
|
+
|
|
129
|
+
export interface WindowsRegistryReaderOptions {
|
|
130
|
+
/** Milliseconds before the registry probe is abandoned. Default 8000. */
|
|
131
|
+
timeoutMs?: number;
|
|
132
|
+
/** Override for tests / non-default shells. */
|
|
133
|
+
pwshPath?: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Reads the registry once per process and memoizes the result. Never throws: a missing shell
|
|
138
|
+
* or non-Windows host degrades to `available: false` so callers fall back to file-based sources.
|
|
139
|
+
*/
|
|
140
|
+
export function createWindowsRegistryReader(options: WindowsRegistryReaderOptions = {}): RegistryReader {
|
|
141
|
+
let pending: Promise<RegistrySnapshot> | undefined;
|
|
142
|
+
return () => {
|
|
143
|
+
if (pending) return pending;
|
|
144
|
+
pending = readRegistrySnapshot(options);
|
|
145
|
+
return pending;
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function readRegistrySnapshot(options: WindowsRegistryReaderOptions): Promise<RegistrySnapshot> {
|
|
150
|
+
if (process.platform !== "win32") {
|
|
151
|
+
return { ...EMPTY, launcherDatPath: "", error: `platform ${process.platform}: registry sources unavailable` };
|
|
152
|
+
}
|
|
153
|
+
const timeoutMs = options.timeoutMs ?? 8000;
|
|
154
|
+
const scriptPath = options.pwshPath ? undefined : join(tmpdir(), "pi-unreal-registry.ps1");
|
|
155
|
+
try {
|
|
156
|
+
if (scriptPath) writeFileSync(scriptPath, PS_SCRIPT, "utf8");
|
|
157
|
+
const command = options.pwshPath ?? "pwsh";
|
|
158
|
+
const args = scriptPath
|
|
159
|
+
? ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", scriptPath]
|
|
160
|
+
: ["-NoProfile", "-NonInteractive", "-Command", PS_SCRIPT];
|
|
161
|
+
const { stdout } = await new Promise<{ stdout: string }>((resolve, reject) => {
|
|
162
|
+
execFile(
|
|
163
|
+
command,
|
|
164
|
+
args,
|
|
165
|
+
{ windowsHide: true, maxBuffer: 8 * 1024 * 1024, timeout: timeoutMs },
|
|
166
|
+
(error, out, err) => {
|
|
167
|
+
if (error) reject(new Error(`${command} failed: ${error.message}${err ? ` | ${err.trim()}` : ""}`));
|
|
168
|
+
else resolve({ stdout: out });
|
|
169
|
+
},
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
const raw = JSON.parse(stdout.trim()) as Partial<RegistrySnapshot> & Record<string, unknown>;
|
|
173
|
+
return {
|
|
174
|
+
available: true,
|
|
175
|
+
hklmVersions: (raw.hklmVersions as HklmEngineKey[]) ?? [],
|
|
176
|
+
hkcuBuilds: (raw.hkcuBuilds as HkcuBuildEntry[]) ?? [],
|
|
177
|
+
installDirDefault: cleanDir(raw.installDirDefault),
|
|
178
|
+
launcherDat: (raw.launcherDat as RegistrySnapshot["launcherDat"]) ?? [],
|
|
179
|
+
launcherDatPath: normalizeOut(raw.launcherDatPath),
|
|
180
|
+
launcherDatExists: Boolean(raw.launcherDatExists),
|
|
181
|
+
envUnrealEngine: cleanDir(raw.envUnrealEngine),
|
|
182
|
+
error: raw.error ? String(raw.error) : undefined,
|
|
183
|
+
};
|
|
184
|
+
} catch (error) {
|
|
185
|
+
return {
|
|
186
|
+
...EMPTY,
|
|
187
|
+
error: error instanceof Error ? error.message : String(error),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function normalizeOut(value: unknown): string {
|
|
193
|
+
if (typeof value !== "string") return "";
|
|
194
|
+
return value.trim().replace(/\\/g, "/").replace(/\/{2,}/g, "/").replace(/\/+$/, "");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function cleanDir(value: unknown): string | undefined {
|
|
198
|
+
const text = normalizeOut(value);
|
|
199
|
+
return text ? text : undefined;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Registry snapshot from data, for tests and for `--engine-root` style overrides. */
|
|
203
|
+
export function createStaticRegistryReader(snapshot: Partial<RegistrySnapshot>): RegistryReader {
|
|
204
|
+
const resolved: RegistrySnapshot = { ...EMPTY, ...snapshot, available: snapshot.available ?? true };
|
|
205
|
+
return async () => resolved;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export const noRegistry: RegistryReader = async () => ({ ...EMPTY, error: "registry sources disabled" });
|