@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.
@@ -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" });