pelican-engine 0.0.0-stage → 0.3.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.
Files changed (192) hide show
  1. package/AGENTS.md +147 -0
  2. package/LICENSE +22 -0
  3. package/README.md +129 -2
  4. package/distribution.json +197 -0
  5. package/docs/AI-WORKFLOW.md +517 -0
  6. package/docs/ARCHITECTURE.md +268 -0
  7. package/docs/CAPABILITIES.md +264 -0
  8. package/docs/DECISIONS.md +107 -0
  9. package/docs/M3-USAGE.md +192 -0
  10. package/docs/PELICAN-MIGRATION.md +38 -0
  11. package/docs/RENDER-BACKENDS.md +50 -0
  12. package/docs/ROADMAP.md +239 -0
  13. package/docs/THIRD-PARTY-NOTICES.md +151 -0
  14. package/docs/api/REFERENCE.md +1435 -0
  15. package/docs/api/index.json +1139 -0
  16. package/docs/npm/DISTRIBUTION.md +130 -0
  17. package/docs/npm/GAME-AUTHOR.md +147 -0
  18. package/engine.manifest.json +15 -0
  19. package/lib/assets/audio.js +67 -0
  20. package/lib/assets/errors.js +36 -0
  21. package/lib/assets/glb.js +277 -0
  22. package/lib/assets/index.js +53 -0
  23. package/lib/assets/jpeg.js +199 -0
  24. package/lib/assets/model.js +558 -0
  25. package/lib/assets/optimize.js +95 -0
  26. package/lib/assets/texture.js +187 -0
  27. package/lib/compiler/distribution.js +17 -0
  28. package/lib/compiler/extract.js +425 -0
  29. package/lib/compiler/graph.js +366 -0
  30. package/lib/compiler/index.js +574 -0
  31. package/lib/compiler/normalize.js +1194 -0
  32. package/lib/compiler/source-map.js +25 -0
  33. package/lib/compiler/types.js +2 -0
  34. package/lib/compiler/version.js +93 -0
  35. package/lib/experiments/simulation/index.js +2 -0
  36. package/lib/experiments/simulation/math.js +41 -0
  37. package/lib/experiments/simulation/probe.js +215 -0
  38. package/lib/experiments/simulation/scenes.js +192 -0
  39. package/lib/experiments/simulation/solver.js +346 -0
  40. package/lib/experiments/simulation/types.js +1 -0
  41. package/lib/experiments/world/adapters.js +30 -0
  42. package/lib/experiments/world/geometry.js +180 -0
  43. package/lib/experiments/world/index.js +2 -0
  44. package/lib/experiments/world/probe.js +288 -0
  45. package/lib/experiments/world/types.js +1 -0
  46. package/lib/experiments/world/world.js +599 -0
  47. package/lib/navigation/index.js +157 -0
  48. package/lib/physics/index.js +909 -0
  49. package/lib/protocol/index.js +92 -0
  50. package/lib/renderer/acceleration.js +609 -0
  51. package/lib/renderer/browser.js +2015 -0
  52. package/lib/renderer/bsdf.js +260 -0
  53. package/lib/renderer/checkpoint.js +88 -0
  54. package/lib/renderer/compute-executor.js +1034 -0
  55. package/lib/renderer/compute-texture-assets.js +49 -0
  56. package/lib/renderer/compute-view-contract.js +1 -0
  57. package/lib/renderer/denoiser.js +361 -0
  58. package/lib/renderer/dielectric.js +360 -0
  59. package/lib/renderer/embedded-image-pixels.js +368 -0
  60. package/lib/renderer/emissive-lights.js +67 -0
  61. package/lib/renderer/environment-importance.js +55 -0
  62. package/lib/renderer/environment.js +346 -0
  63. package/lib/renderer/gameplay.js +464 -0
  64. package/lib/renderer/geometry.js +103 -0
  65. package/lib/renderer/graph-expressions.js +485 -0
  66. package/lib/renderer/graph-tsl.js +141 -0
  67. package/lib/renderer/graph.js +214 -0
  68. package/lib/renderer/managed-texture-sampling.js +29 -0
  69. package/lib/renderer/material-graphs.js +297 -0
  70. package/lib/renderer/medium.js +348 -0
  71. package/lib/renderer/models.js +1053 -0
  72. package/lib/renderer/motion-shader.js +40 -0
  73. package/lib/renderer/path-shader.js +196 -0
  74. package/lib/renderer/pathtrace.js +324 -0
  75. package/lib/renderer/postprocess-contract.js +1 -0
  76. package/lib/renderer/postprocess.js +494 -0
  77. package/lib/renderer/render-buffers.js +132 -0
  78. package/lib/renderer/render-textures.js +155 -0
  79. package/lib/renderer/startup.js +50 -0
  80. package/lib/renderer/surface-capture.js +199 -0
  81. package/lib/renderer/surface-gpu.js +270 -0
  82. package/lib/renderer/surface-motion.js +84 -0
  83. package/lib/renderer/surface-ray-shader.js +91 -0
  84. package/lib/renderer/surface-shader.js +88 -0
  85. package/lib/renderer/surface-tracer.js +1720 -0
  86. package/lib/renderer/surface.js +1425 -0
  87. package/lib/renderer/temporal-filter.js +512 -0
  88. package/lib/renderer/texture-pixels.js +207 -0
  89. package/lib/renderer/traversal-nodes.js +54 -0
  90. package/lib/renderer/ui.js +446 -0
  91. package/lib/renderer/vertex-bounds.js +127 -0
  92. package/lib/renderer/vertex-contract.js +1 -0
  93. package/lib/renderer/vertex-graphs.js +197 -0
  94. package/lib/renderer/volume-shader.js +50 -0
  95. package/lib/runtime/compute-contract.js +1 -0
  96. package/lib/runtime/compute-service.js +674 -0
  97. package/lib/runtime/contracts.js +1 -0
  98. package/lib/runtime/error.js +37 -0
  99. package/lib/runtime/events.js +189 -0
  100. package/lib/runtime/game-services.js +772 -0
  101. package/lib/runtime/host.js +839 -0
  102. package/lib/runtime/material-parameters.js +145 -0
  103. package/lib/runtime/plugins.js +1 -0
  104. package/lib/runtime/postprocess-contract.js +1 -0
  105. package/lib/runtime/postprocess.js +189 -0
  106. package/lib/runtime/prefabs.js +13 -0
  107. package/lib/runtime/random.js +88 -0
  108. package/lib/runtime/render-buffer-contract.js +1 -0
  109. package/lib/runtime/render-buffers.js +243 -0
  110. package/lib/runtime/render-capabilities.js +17 -0
  111. package/lib/runtime/render-settings.js +249 -0
  112. package/lib/runtime/render-texture-contract.js +1 -0
  113. package/lib/runtime/render-textures.js +271 -0
  114. package/lib/runtime/resources.js +648 -0
  115. package/lib/runtime/scheduler.js +765 -0
  116. package/lib/runtime/storage.js +169 -0
  117. package/lib/runtime/world.js +594 -0
  118. package/lib/schema/index.js +24 -0
  119. package/lib/sdk/graph-values.js +16 -0
  120. package/lib/sdk/index.d.ts +800 -0
  121. package/lib/sdk/index.js +82 -0
  122. package/lib/tools/api-index.js +113 -0
  123. package/lib/tools/browser-session.js +89 -0
  124. package/lib/tools/cli.js +513 -0
  125. package/lib/tools/compress-dist.js +59 -0
  126. package/lib/tools/control-client.js +153 -0
  127. package/lib/tools/control-server.js +633 -0
  128. package/lib/tools/diagnostic-code.js +3 -0
  129. package/lib/tools/doctor.js +496 -0
  130. package/lib/tools/npm/browser-cli.js +6 -0
  131. package/lib/tools/npm/entry.js +40 -0
  132. package/lib/tools/npm/layout.js +195 -0
  133. package/lib/tools/setup.js +153 -0
  134. package/licenses/@dimforge/rapier2d-compat/LICENSE +201 -0
  135. package/licenses/@dimforge/rapier3d-compat/LICENSE +201 -0
  136. package/licenses/@gltf-transform/core/LICENSE.md +21 -0
  137. package/licenses/@gltf-transform/extensions/LICENSE.md +21 -0
  138. package/licenses/@gltf-transform/functions/LICENSE.md +21 -0
  139. package/licenses/@img/colour/LICENSE.md +82 -0
  140. package/licenses/@img/sharp-win32-x64/LICENSE +191 -0
  141. package/licenses/@oxc-project/types/LICENSE +22 -0
  142. package/licenses/@rolldown/pluginutils/LICENSE +21 -0
  143. package/licenses/@tweenjs/tween.js/LICENSE +23 -0
  144. package/licenses/@types/ndarray/LICENSE +21 -0
  145. package/licenses/@types/node/LICENSE +21 -0
  146. package/licenses/@types/stats.js/LICENSE +21 -0
  147. package/licenses/@types/three/@dimforge/rapier3d-compat/LICENSE +201 -0
  148. package/licenses/@types/three/LICENSE +21 -0
  149. package/licenses/@types/webxr/LICENSE +21 -0
  150. package/licenses/@webgpu/types/LICENSE +26 -0
  151. package/licenses/THIRD-PARTY-NOTICES.md +139 -0
  152. package/licenses/cwise-compiler/LICENSE +22 -0
  153. package/licenses/detect-libc/LICENSE +201 -0
  154. package/licenses/esbuild/LICENSE.md +21 -0
  155. package/licenses/fast-png/LICENSE +21 -0
  156. package/licenses/fdir/LICENSE +7 -0
  157. package/licenses/fflate/LICENSE +21 -0
  158. package/licenses/iobuffer/LICENSE +22 -0
  159. package/licenses/iota-array/LICENSE +22 -0
  160. package/licenses/is-buffer/LICENSE +21 -0
  161. package/licenses/ktx-parse/LICENSE +21 -0
  162. package/licenses/lightningcss/LICENSE +373 -0
  163. package/licenses/lightningcss-win32-x64-msvc/LICENSE +373 -0
  164. package/licenses/meshoptimizer/LICENSE.md +21 -0
  165. package/licenses/nanoid/LICENSE +20 -0
  166. package/licenses/ndarray/LICENSE +22 -0
  167. package/licenses/ndarray-lanczos/LICENSE +21 -0
  168. package/licenses/ndarray-ops/LICENSE +22 -0
  169. package/licenses/ndarray-pixels/LICENSE +21 -0
  170. package/licenses/ngraph.path/LICENSE +21 -0
  171. package/licenses/picocolors/LICENSE +15 -0
  172. package/licenses/picomatch/LICENSE +21 -0
  173. package/licenses/playwright/LICENSE +202 -0
  174. package/licenses/playwright/NOTICE +5 -0
  175. package/licenses/playwright-core/LICENSE +202 -0
  176. package/licenses/playwright-core/NOTICE +5 -0
  177. package/licenses/postcss/LICENSE +20 -0
  178. package/licenses/prettier/LICENSE +7 -0
  179. package/licenses/property-graph/LICENSE +21 -0
  180. package/licenses/rolldown/LICENSE +25 -0
  181. package/licenses/semver/LICENSE +15 -0
  182. package/licenses/sharp/LICENSE +191 -0
  183. package/licenses/source-map-js/LICENSE +28 -0
  184. package/licenses/three/LICENSE +21 -0
  185. package/licenses/tinyglobby/LICENSE +21 -0
  186. package/licenses/tsx/LICENSE +21 -0
  187. package/licenses/typescript/LICENSE.txt +55 -0
  188. package/licenses/undici-types/LICENSE +21 -0
  189. package/licenses/uniq/LICENSE +22 -0
  190. package/licenses/vite/LICENSE.md +2084 -0
  191. package/package.json +49 -4
  192. package/web/index.html +34 -0
@@ -0,0 +1,496 @@
1
+ /** Read-only M0 environment baseline. Public reports never contain raw command output. */
2
+ import { execFile } from "node:child_process";
3
+ import { existsSync } from "node:fs";
4
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
5
+ import { constants } from "node:fs";
6
+ import { createRequire } from "node:module";
7
+ import { homedir } from "node:os";
8
+ import path from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ export function diagnostic(code, severity, message, remediation) {
11
+ return { code, severity, message, remediation };
12
+ }
13
+ export function envelope(taskId, diagnostics, data) {
14
+ return {
15
+ protocolVersion: "0.1.0",
16
+ taskId,
17
+ status: diagnostics.some((d) => d.severity === "error")
18
+ ? "failed"
19
+ : "succeeded",
20
+ diagnostics,
21
+ artifacts: [],
22
+ data,
23
+ };
24
+ }
25
+ /** Redacts paths recursively, without serializing process.env or OS account properties. */
26
+ export function sanitizeReport(value, projectRoot) {
27
+ const root = path.resolve(projectRoot);
28
+ const home = homedir();
29
+ const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
30
+ const replacePath = (s, target, token) => s.replace(new RegExp(`${escape(target)}(?=$|[\\\\/])`, "gi"), token);
31
+ const visit = (input) => {
32
+ if (typeof input === "string") {
33
+ let s = replacePath(input, root, "<project>");
34
+ s = replacePath(s, home, "<home>");
35
+ return s
36
+ .replace(/([A-Z]:[\\/]Users[\\/])[^\\/\s]+/gi, "$1<user>")
37
+ .replace(/(https?:\/\/)[^\s/@]+(?::[^\s/@]*)?@/gi, "$1<credentials>@")
38
+ .replace(/[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}/gi, "<email>");
39
+ }
40
+ if (Array.isArray(input))
41
+ return input.map(visit);
42
+ if (input && typeof input === "object")
43
+ return Object.fromEntries(Object.entries(input).map(([k, v]) => [k, visit(v)]));
44
+ return input;
45
+ };
46
+ return visit(value);
47
+ }
48
+ export const runCommand = async (file, args, options) => new Promise((resolve) => {
49
+ execFile(file, [...args], {
50
+ cwd: options.cwd,
51
+ timeout: options.timeoutMs,
52
+ signal: options.signal,
53
+ windowsHide: true,
54
+ encoding: "utf8",
55
+ maxBuffer: 2 * 1024 * 1024,
56
+ }, (error, stdout, stderr) => {
57
+ if (!error)
58
+ return resolve({ exitCode: 0, stdout, stderr });
59
+ const err = error;
60
+ resolve({
61
+ exitCode: typeof err.code === "number" ? err.code : null,
62
+ stdout,
63
+ stderr,
64
+ failure: options.signal?.aborted
65
+ ? "aborted"
66
+ : err.code === "ENOENT"
67
+ ? "missing"
68
+ : err.killed
69
+ ? "timeout"
70
+ : "failed",
71
+ });
72
+ });
73
+ });
74
+ /** Locate npm's JS CLI rather than passing unescaped directory names through cmd.exe. */
75
+ export function npmCommand(projectRoot) {
76
+ const candidates = [
77
+ process.env.npm_execpath,
78
+ path.join(path.dirname(process.execPath), "node_modules/npm/bin/npm-cli.js"),
79
+ ...(process.env.PATH ?? "")
80
+ .split(path.delimiter)
81
+ .filter(Boolean)
82
+ .map((p) => path.join(p, "node_modules/npm/bin/npm-cli.js")),
83
+ ];
84
+ for (const candidate of candidates) {
85
+ if (candidate && /npm-cli\.js$/i.test(candidate) && existsSync(candidate))
86
+ return { file: process.execPath, prefix: [candidate] };
87
+ }
88
+ try {
89
+ const require = createRequire(path.join(projectRoot, "package.json"));
90
+ return {
91
+ file: process.execPath,
92
+ prefix: [require.resolve("npm/bin/npm-cli.js")],
93
+ };
94
+ }
95
+ catch {
96
+ return null;
97
+ }
98
+ }
99
+ async function jsonFile(file) {
100
+ const value = JSON.parse((await readFile(file, "utf8")).replace(/^\uFEFF/, ""));
101
+ if (!value || typeof value !== "object" || Array.isArray(value))
102
+ throw new Error("Expected JSON object");
103
+ return value;
104
+ }
105
+ /** Checks all direct locked dependencies, not merely package.json ranges or folder presence. */
106
+ export async function inspectDependencies(projectRoot) {
107
+ const diagnostics = [];
108
+ const state = {
109
+ manifestPresent: false,
110
+ lockPresent: false,
111
+ lockUsable: false,
112
+ ready: false,
113
+ packages: [],
114
+ diagnostics,
115
+ };
116
+ let manifest;
117
+ let lock;
118
+ try {
119
+ manifest = await jsonFile(path.join(projectRoot, "package.json"));
120
+ state.manifestPresent = true;
121
+ }
122
+ catch {
123
+ diagnostics.push(diagnostic("TCENV_MANIFEST_INVALID", "error", "package.json is missing or invalid.", "Use an intact pelican directory containing package.json."));
124
+ return state;
125
+ }
126
+ try {
127
+ lock = await jsonFile(path.join(projectRoot, "package-lock.json"));
128
+ state.lockPresent = true;
129
+ }
130
+ catch {
131
+ diagnostics.push(diagnostic("TCENV_LOCK_INVALID", "error", "package-lock.json is missing or invalid.", "Restore the supplied lockfile; setup never generates or upgrades a lockfile."));
132
+ return state;
133
+ }
134
+ if (!lock.packages ||
135
+ typeof lock.packages !== "object" ||
136
+ !lock.packages[""] ||
137
+ ![2, 3].includes(lock.lockfileVersion)) {
138
+ diagnostics.push(diagnostic("TCENV_LOCK_FORMAT", "error", "The lockfile must contain npm v2/v3 package records.", "Restore the matching npm lockfile before setup."));
139
+ return state;
140
+ }
141
+ const requested = { ...manifest.dependencies, ...manifest.devDependencies };
142
+ const lockedRoot = {
143
+ ...lock.packages[""].dependencies,
144
+ ...lock.packages[""].devDependencies,
145
+ };
146
+ const required = ["tsx", "typescript", "playwright", "three", "vite"];
147
+ for (const name of [
148
+ ...new Set([
149
+ ...Object.keys(requested),
150
+ ...Object.keys(lockedRoot),
151
+ ...required,
152
+ ]),
153
+ ].sort()) {
154
+ if (!/^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/i.test(name)) {
155
+ diagnostics.push(diagnostic("TCENV_DEPENDENCY_NAME", "error", "A dependency has an invalid package name.", "Correct package.json and its lockfile through the engine dependency workflow."));
156
+ continue;
157
+ }
158
+ const entry = lock.packages[`node_modules/${name}`];
159
+ const version = typeof entry?.version === "string" ? entry.version : null;
160
+ let installed = null;
161
+ try {
162
+ const found = (await jsonFile(path.join(projectRoot, "node_modules", name, "package.json"))).version;
163
+ installed = typeof found === "string" ? found : null;
164
+ }
165
+ catch {
166
+ /* Missing, truncated, or invalid installations all require repair. */
167
+ }
168
+ state.packages.push({
169
+ name,
170
+ requested: requested[name] ?? null,
171
+ locked: version,
172
+ installed,
173
+ });
174
+ if (!requested[name] ||
175
+ requested[name] !== lockedRoot[name] ||
176
+ !version ||
177
+ (/^\d+\.\d+\.\d+(?:[-+][\w.-]+)?$/.test(requested[name]) &&
178
+ requested[name] !== version)) {
179
+ diagnostics.push(diagnostic("TCENV_LOCK_MISMATCH", "error", `The lockfile does not match the requested dependency ${name}.`, "Restore matching package.json and package-lock.json; do not upgrade implicitly."));
180
+ }
181
+ else if (installed !== version) {
182
+ diagnostics.push(diagnostic("TCENV_DEPENDENCY_NOT_READY", "error", `Dependency ${name} is missing or differs from its locked version.`, "Run setup to restore node_modules using npm ci."));
183
+ }
184
+ }
185
+ // npm records the complete installed tree; checking transitives catches incomplete cached installs.
186
+ for (const [location, entry] of Object.entries(lock.packages)) {
187
+ if (!location ||
188
+ !location.startsWith("node_modules/") ||
189
+ entry.link ||
190
+ entry.optional)
191
+ continue;
192
+ const resolved = path.resolve(projectRoot, location, "package.json");
193
+ const relative = path.relative(projectRoot, resolved);
194
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
195
+ diagnostics.push(diagnostic("TCENV_LOCK_PATH", "error", "A lockfile package path escapes the project.", "Restore the supplied lockfile."));
196
+ continue;
197
+ }
198
+ // Other-platform optional packages are skipped above; active required packages must exist.
199
+ try {
200
+ const actual = await jsonFile(resolved);
201
+ if (actual.version !== entry.version)
202
+ throw new Error("Version mismatch");
203
+ }
204
+ catch {
205
+ if (!Object.hasOwn(requested, location.slice("node_modules/".length))) {
206
+ diagnostics.push(diagnostic("TCENV_TRANSITIVE_NOT_READY", "error", `A required locked transitive dependency is missing or inconsistent: ${location}.`, "Run setup to repair the dependency tree with npm ci."));
207
+ }
208
+ }
209
+ }
210
+ state.lockUsable = !diagnostics.some((d) => [
211
+ "TCENV_LOCK_MISMATCH",
212
+ "TCENV_LOCK_PATH",
213
+ "TCENV_DEPENDENCY_NAME",
214
+ ].includes(d.code));
215
+ state.ready = !diagnostics.some((d) => d.severity === "error");
216
+ return state;
217
+ }
218
+ const numeric = (v) => (typeof v === "number" || typeof v === "string") &&
219
+ String(v).trim() !== "" &&
220
+ Number.isFinite(Number(v)) &&
221
+ Number(v) >= 0
222
+ ? Number(v)
223
+ : null;
224
+ const text = (v) => typeof v === "string" && v.trim() ? v.trim() : null;
225
+ const array = (v) => v == null ? [] : Array.isArray(v) ? v : [v];
226
+ /** Projects selected hardware properties only: never computer name, serials, user or owner. */
227
+ export function parseCim(stdout) {
228
+ const raw = JSON.parse(stdout.replace(/^\uFEFF/, "").trim());
229
+ if (!raw?.computer ||
230
+ !raw?.os ||
231
+ !numeric(raw.computer.TotalPhysicalMemory) ||
232
+ !text(raw.os.Version) ||
233
+ !array(raw.cpus).length)
234
+ throw new Error("Incomplete CIM inventory");
235
+ return {
236
+ manufacturer: text(raw.computer.Manufacturer),
237
+ model: text(raw.computer.Model),
238
+ ramBytes: numeric(raw.computer.TotalPhysicalMemory),
239
+ os: {
240
+ caption: text(raw.os.Caption),
241
+ version: text(raw.os.Version),
242
+ build: text(raw.os.BuildNumber),
243
+ architecture: text(raw.os.OSArchitecture),
244
+ },
245
+ cpus: array(raw.cpus).map((c) => ({
246
+ name: text(c.Name),
247
+ cores: numeric(c.NumberOfCores),
248
+ logicalProcessors: numeric(c.NumberOfLogicalProcessors),
249
+ maxClockMHz: numeric(c.MaxClockSpeed),
250
+ })),
251
+ gpus: array(raw.gpus).map((g) => ({
252
+ name: text(g.Name),
253
+ driverVersion: text(g.DriverVersion),
254
+ adapterRamReportedBytes: numeric(g.AdapterRAM),
255
+ adapterRamReliable: false,
256
+ dedicatedMemoryBytes: null,
257
+ dedicatedMemorySource: null,
258
+ })),
259
+ };
260
+ }
261
+ /** nvidia-smi memory.total uses MiB; do not round or substitute the advertised 12 GB. */
262
+ export function parseNvidiaSmi(stdout) {
263
+ const lines = stdout.trim().split(/\r?\n/).filter(Boolean);
264
+ if (!lines.length)
265
+ throw new Error("Empty NVIDIA inventory");
266
+ return lines.map((line) => {
267
+ const match = /^\s*("(?:[^"]|"")*"|[^,]+),\s*([^,]+),\s*([\d.]+)\s*$/.exec(line);
268
+ const memoryMiB = match ? Number(match[3]) : NaN;
269
+ if (!match || !Number.isFinite(memoryMiB) || memoryMiB <= 0)
270
+ throw new Error("Invalid NVIDIA inventory");
271
+ return {
272
+ name: match[1].trim().replace(/^"|"$/g, "").replace(/""/g, '"'),
273
+ driverVersion: match[2].trim(),
274
+ memoryMiB,
275
+ memoryBytes: memoryMiB * 1024 * 1024,
276
+ };
277
+ });
278
+ }
279
+ const cimScript = `
280
+ $ErrorActionPreference = 'Stop'
281
+ [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
282
+ $inventory = @{
283
+ computer = Get-CimInstance Win32_ComputerSystem | Select-Object Manufacturer,Model,TotalPhysicalMemory
284
+ os = Get-CimInstance Win32_OperatingSystem | Select-Object Caption,Version,BuildNumber,OSArchitecture
285
+ cpus = @(Get-CimInstance Win32_Processor | Select-Object Name,NumberOfCores,NumberOfLogicalProcessors,MaxClockSpeed)
286
+ gpus = @(Get-CimInstance Win32_VideoController | Select-Object Name,DriverVersion,AdapterRAM)
287
+ }
288
+ $inventory | ConvertTo-Json -Depth 6 -Compress
289
+ `;
290
+ async function browserInfo(projectRoot, diagnostics) {
291
+ const cache = process.env.PLAYWRIGHT_BROWSERS_PATH;
292
+ const cachePath = cache === "0"
293
+ ? "<playwright-core>/.local-browsers"
294
+ : cache
295
+ ? path.resolve(projectRoot, cache)
296
+ : path.join(process.env.LOCALAPPDATA ?? path.join(homedir(), "AppData/Local"), "ms-playwright");
297
+ const data = {
298
+ provider: "playwright-chromium",
299
+ installed: false,
300
+ executablePath: null,
301
+ cachePath,
302
+ pathNotation: "project-and-home-tokens",
303
+ launched: false,
304
+ };
305
+ try {
306
+ const require = createRequire(path.join(projectRoot, "package.json"));
307
+ const playwright = require("playwright");
308
+ data.executablePath = playwright.chromium.executablePath();
309
+ await access(data.executablePath, constants.R_OK);
310
+ data.installed = true;
311
+ }
312
+ catch {
313
+ diagnostics.push(diagnostic("TCENV_BROWSER_MISSING", "error", "The locked Playwright Chromium executable is unavailable.", "Install the bundled browser with node node_modules/playwright/cli.js install chromium, then rerun doctor."));
314
+ }
315
+ return data;
316
+ }
317
+ /** Read-only. Defaults to cwd; the caller decides whether/where to persist the report. */
318
+ export async function doctor(options = {}) {
319
+ const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
320
+ const diagnostics = [];
321
+ const runner = options.runCommand ?? runCommand;
322
+ const commandOptions = {
323
+ cwd: projectRoot,
324
+ timeoutMs: options.timeoutMs ?? 20_000,
325
+ ...(options.signal ? { signal: options.signal } : {}),
326
+ };
327
+ const safeRun = async (file, args) => {
328
+ try {
329
+ return await runner(file, args, commandOptions);
330
+ }
331
+ catch {
332
+ return { exitCode: null, stdout: "", failure: "failed" };
333
+ }
334
+ };
335
+ if (process.platform !== "win32")
336
+ diagnostics.push(diagnostic("TCENV_PLATFORM_UNSUPPORTED", "error", "The development toolchain currently supports Windows.", "Run development checks on Windows; this is not a player browser restriction."));
337
+ const npm = npmCommand(projectRoot);
338
+ const [cim, nvidia, npmVersion, git, dependencies, browser] = await Promise.all([
339
+ safeRun("powershell.exe", [
340
+ "-NoLogo",
341
+ "-NoProfile",
342
+ "-NonInteractive",
343
+ "-Command",
344
+ cimScript,
345
+ ]),
346
+ safeRun("nvidia-smi", [
347
+ "--query-gpu=name,driver_version,memory.total",
348
+ "--format=csv,noheader,nounits",
349
+ ]),
350
+ npm
351
+ ? safeRun(npm.file, [...npm.prefix, "--version"])
352
+ : Promise.resolve({
353
+ exitCode: null,
354
+ stdout: "",
355
+ failure: "missing",
356
+ }),
357
+ safeRun("git", ["--version"]),
358
+ inspectDependencies(projectRoot),
359
+ browserInfo(projectRoot, diagnostics),
360
+ ]);
361
+ diagnostics.push(...dependencies.diagnostics);
362
+ let machine = null;
363
+ if (cim.exitCode === 0) {
364
+ try {
365
+ machine = parseCim(cim.stdout);
366
+ }
367
+ catch {
368
+ diagnostics.push(diagnostic("TCENV_CIM_INVALID", "error", "Windows returned an incomplete or invalid hardware inventory.", "Retry doctor; verify the Windows CIM service is available."));
369
+ }
370
+ }
371
+ else
372
+ diagnostics.push(diagnostic("TCENV_CIM_UNAVAILABLE", "error", "Windows CIM inventory could not be collected.", "Retry doctor with PowerShell and the local CIM service available; no administrator installation is performed."));
373
+ let nvidiaGpus = [];
374
+ if (nvidia.exitCode === 0) {
375
+ try {
376
+ nvidiaGpus = parseNvidiaSmi(nvidia.stdout);
377
+ }
378
+ catch {
379
+ diagnostics.push(diagnostic("TCENV_NVIDIA_INVALID", "warning", "nvidia-smi returned unsupported inventory data.", "Check nvidia-smi output locally; exact dedicated memory remains unknown."));
380
+ }
381
+ }
382
+ else
383
+ diagnostics.push(diagnostic("TCENV_NVIDIA_UNAVAILABLE", "warning", "nvidia-smi is unavailable; exact NVIDIA dedicated memory was not measured.", "Use the vendor inventory tool if applicable. Non-NVIDIA hardware is not rejected."));
384
+ if (machine) {
385
+ for (const gpu of machine.gpus) {
386
+ const exact = nvidiaGpus.find((n) => n.name.toLowerCase() === gpu.name?.toLowerCase());
387
+ if (exact) {
388
+ gpu.dedicatedMemoryBytes = exact.memoryBytes;
389
+ gpu.dedicatedMemorySource = "nvidia-smi";
390
+ }
391
+ }
392
+ if (machine.gpus.length)
393
+ diagnostics.push(diagnostic("TCENV_ADAPTER_RAM_UNRELIABLE", "info", "CIM AdapterRAM is a 32-bit field and can truncate VRAM above 4 GiB; it is not used as dedicated memory.", "Use the separately recorded vendor memory measurement, not AdapterRAM."));
394
+ }
395
+ const npmText = npmVersion.exitCode === 0 &&
396
+ /^\d+\.\d+\.\d+(?:[-+][\w.-]+)?$/.test(npmVersion.stdout.trim())
397
+ ? npmVersion.stdout.trim()
398
+ : null;
399
+ if (!npmText)
400
+ diagnostics.push(diagnostic("TCENV_NPM_UNAVAILABLE", "error", "The npm CLI is missing or did not return a valid version.", "Use a local or user-level Node distribution containing npm, then rerun setup."));
401
+ const gitText = git.exitCode === 0
402
+ ? (/^git version [\w.+()-]+/m.exec(git.stdout)?.[0] ?? null)
403
+ : null;
404
+ if (!gitText)
405
+ diagnostics.push(diagnostic("TCENV_GIT_OPTIONAL", "info", "Git is unavailable; ZIP and directory workflows remain supported.", "No action required for ZIP or directory use."));
406
+ if (options.signal?.aborted)
407
+ diagnostics.push(diagnostic("TCENV_CANCELLED", "error", "Environment collection was cancelled.", "Rerun doctor when ready."));
408
+ const data = {
409
+ capturedAt: new Date().toISOString(),
410
+ platform: process.platform,
411
+ architecture: process.arch,
412
+ machine,
413
+ nvidia: nvidiaGpus,
414
+ runtime: {
415
+ node: process.versions.node,
416
+ npm: npmText,
417
+ nodeExecutable: process.execPath,
418
+ },
419
+ dependencies,
420
+ browser,
421
+ git: {
422
+ available: !!gitText,
423
+ version: gitText,
424
+ checkout: existsSync(path.join(projectRoot, ".git")),
425
+ },
426
+ };
427
+ return sanitizeReport(envelope(options.taskId ?? "M0-00-environment", diagnostics, data), projectRoot);
428
+ }
429
+ /** CLI helper used by both tools. Library calls never write evidence automatically. */
430
+ export async function printReport(report, output, projectRoot) {
431
+ if (output) {
432
+ const destination = path.resolve(projectRoot, output);
433
+ const rel = path.relative(projectRoot, destination);
434
+ if (rel.startsWith(`..${path.sep}`) ||
435
+ rel === ".." ||
436
+ path.isAbsolute(rel)) {
437
+ report.diagnostics.push(diagnostic("TCENV_OUTPUT_PATH", "error", "Report output must stay inside the project directory.", "Choose a project-relative output path."));
438
+ report.status = "failed";
439
+ }
440
+ else {
441
+ report.artifacts.push({
442
+ type: "application/json",
443
+ path: rel.split(path.sep).join("/"),
444
+ });
445
+ try {
446
+ await mkdir(path.dirname(destination), { recursive: true });
447
+ await writeFile(destination, `${JSON.stringify(report, null, 2)}\n`, "utf8");
448
+ }
449
+ catch {
450
+ report.artifacts.pop();
451
+ report.status = "failed";
452
+ report.diagnostics.push(diagnostic("TCENV_OUTPUT_WRITE_FAILED", "error", "The report file could not be written.", "Check output directory permissions and rerun the command."));
453
+ }
454
+ }
455
+ }
456
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
457
+ process.exitCode = report.status === "succeeded" ? 0 : 1;
458
+ }
459
+ export function isDirectInvocation(metaUrl) {
460
+ return (!!process.argv[1] &&
461
+ path.resolve(fileURLToPath(metaUrl)).toLowerCase() ===
462
+ path.resolve(process.argv[1]).toLowerCase());
463
+ }
464
+ export function parseArguments(args, flags = []) {
465
+ const result = { flags: new Set() };
466
+ for (let i = 0; i < args.length; i++) {
467
+ const arg = args[i];
468
+ if (arg === "--output" || arg === "--project") {
469
+ const value = args[++i];
470
+ if (!value || value.startsWith("--"))
471
+ throw new Error("Missing argument");
472
+ if (arg === "--output")
473
+ result.output = value;
474
+ else
475
+ result.projectRoot = value;
476
+ }
477
+ else if (flags.includes(arg))
478
+ result.flags.add(arg);
479
+ else
480
+ throw new Error("Unknown argument");
481
+ }
482
+ return result;
483
+ }
484
+ if (isDirectInvocation(import.meta.url)) {
485
+ let projectRoot = process.cwd();
486
+ try {
487
+ const args = parseArguments(process.argv.slice(2));
488
+ projectRoot = path.resolve(args.projectRoot ?? process.cwd());
489
+ await printReport(await doctor({ projectRoot }), args.output, projectRoot);
490
+ }
491
+ catch {
492
+ await printReport(envelope("M0-00-environment", [
493
+ diagnostic("TCENV_ARGUMENT_OR_INTERNAL_ERROR", "error", "The command could not complete.", "Use doctor [--project directory] [--output relative-file.json] and check the project directory."),
494
+ ], null), undefined, projectRoot);
495
+ }
496
+ }
@@ -0,0 +1,6 @@
1
+ import { createRequire } from "node:module";
2
+ import path from "node:path";
3
+ export function playwrightCli(engineRoot) {
4
+ const require = createRequire(path.join(engineRoot, "package.json"));
5
+ return path.join(path.dirname(require.resolve("playwright/package.json")), "cli.js");
6
+ }
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ import { fileURLToPath } from "node:url";
3
+ // Keep the executable boundary free of third-party imports so installation errors
4
+ // have the same machine-readable protocol as compiler and command failures.
5
+ try {
6
+ const root = fileURLToPath(new URL("../../../", import.meta.url));
7
+ const { installedDiagnostics } = await import("./layout.js");
8
+ const diagnostics = await installedDiagnostics(root);
9
+ if (diagnostics.length) {
10
+ console.log(JSON.stringify({
11
+ protocolVersion: "0.1.0",
12
+ taskId: "installation",
13
+ status: "failed",
14
+ diagnostics,
15
+ artifacts: [],
16
+ data: null,
17
+ }, null, 2));
18
+ process.exitCode = 1;
19
+ }
20
+ else
21
+ await import("../cli.js");
22
+ }
23
+ catch (error) {
24
+ console.log(JSON.stringify({
25
+ protocolVersion: "0.1.0",
26
+ taskId: "installation",
27
+ status: "failed",
28
+ diagnostics: [
29
+ {
30
+ code: "TC_DISTRIBUTION_INSTALLATION",
31
+ severity: "error",
32
+ message: String(error),
33
+ remediation: "Reinstall the exact pelican-engine package and its dependencies; do not copy partial node_modules directories.",
34
+ },
35
+ ],
36
+ artifacts: [],
37
+ data: null,
38
+ }, null, 2));
39
+ process.exitCode = 1;
40
+ }