@forgeax/engine-devkit 0.1.6 → 0.1.7
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 +143 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/engine-binding.unit.test.d.ts +2 -0
- package/dist/__tests__/engine-binding.unit.test.d.ts.map +1 -0
- package/dist/__tests__/software-browser-session.unit.test.d.ts +2 -0
- package/dist/__tests__/software-browser-session.unit.test.d.ts.map +1 -0
- package/dist/__tests__/software-capture-pixels.unit.test.d.ts +2 -0
- package/dist/__tests__/software-capture-pixels.unit.test.d.ts.map +1 -0
- package/dist/__tests__/software-capture.unit.test.d.ts +2 -0
- package/dist/__tests__/software-capture.unit.test.d.ts.map +1 -0
- package/dist/cli-output.d.ts.map +1 -1
- package/dist/cli.mjs +1477 -246
- package/dist/cli.mjs.map +1 -1
- package/dist/commands.d.ts +2 -0
- package/dist/commands.d.ts.map +1 -1
- package/dist/engine-binding.d.ts +58 -0
- package/dist/engine-binding.d.ts.map +1 -0
- package/dist/host.d.ts.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +1414 -295
- package/dist/index.mjs.map +1 -1
- package/dist/rhi-debug/cli-context.d.ts.map +1 -1
- package/dist/rhi-debug/operations.d.ts +3 -3
- package/dist/rhi-debug/operations.d.ts.map +1 -1
- package/dist/sdk-bootstrap.d.ts.map +1 -1
- package/dist/sdk-cli.mjs +4 -2
- package/dist/sdk-cli.mjs.map +1 -1
- package/dist/software-capture.d.ts +123 -0
- package/dist/software-capture.d.ts.map +1 -0
- package/dist/tools/browser-host.d.ts.map +1 -1
- package/dist/tools/commands.d.ts +5 -0
- package/dist/tools/commands.d.ts.map +1 -1
- package/dist/types.d.ts +21 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +32 -30
package/dist/cli.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { deflateRawSync } from 'zlib';
|
|
3
|
-
import { createHash } from 'crypto';
|
|
4
|
-
import { readFile, mkdtemp, rm, readdir, mkdir, cp, rename, writeFile, copyFile, stat, symlink, unlink,
|
|
3
|
+
import { createHash, randomUUID } from 'crypto';
|
|
4
|
+
import { readFile, mkdtemp, rm, readdir, mkdir, cp, rename, writeFile, realpath, copyFile, stat, symlink, unlink, access, readlink, lstat, rmdir } from 'fs/promises';
|
|
5
5
|
import { resolve, extname, dirname, basename, relative, isAbsolute, sep } from 'path';
|
|
6
|
+
import { GameProjectSchema } from '@forgeax/engine-project';
|
|
6
7
|
import { readFileSync, existsSync } from 'fs';
|
|
7
8
|
import { createRequire } from 'module';
|
|
8
9
|
import { pathToFileURL, fileURLToPath } from 'url';
|
|
@@ -19,17 +20,19 @@ import { createStandaloneRuntimeAssetBinding, ok, err } from '@forgeax/engine-ty
|
|
|
19
20
|
import { createParticleCodeNativeCookerFromRoots } from '@forgeax/engine-vfx-compiler';
|
|
20
21
|
import { pluginPack, reloadAssetHost } from '@forgeax/engine-vite-plugin-pack';
|
|
21
22
|
import { forgeaxShader } from '@forgeax/engine-vite-plugin-shader';
|
|
22
|
-
import { GameProjectSchema } from '@forgeax/engine-project';
|
|
23
23
|
import { runCliGltf } from '@forgeax/engine-gltf/cli-gltf';
|
|
24
24
|
import { scanEntries } from '@forgeax/engine-pack/cli-asset';
|
|
25
25
|
import { AssetGuid } from '@forgeax/engine-pack/guid';
|
|
26
|
-
import { execFile } from 'child_process';
|
|
26
|
+
import { spawn, execFile } from 'child_process';
|
|
27
27
|
import { tmpdir } from 'os';
|
|
28
28
|
import { promisify } from 'util';
|
|
29
|
-
import { decodeTape, openReplay, buildFrameModel, createRhiDebugError } from '@forgeax/engine-rhi-debug';
|
|
30
|
-
import { rhi, createShaderModule } from '@forgeax/engine-rhi-
|
|
29
|
+
import { decodeTape, openReplay, buildFrameModel, replayDeviceRequest, createRhiDebugError } from '@forgeax/engine-rhi-debug';
|
|
30
|
+
import { rhi, createShaderModule } from '@forgeax/engine-rhi-webgpu';
|
|
31
31
|
import { build, preview, createServer as createServer$1 } from 'vite';
|
|
32
32
|
import { createServer } from 'net';
|
|
33
|
+
import { FORGEAX_FRAME_SUBMITTED_DATASET } from '@forgeax/engine-app';
|
|
34
|
+
import { parseImage } from '@forgeax/engine-image/parse-image';
|
|
35
|
+
import { chromium } from 'playwright';
|
|
33
36
|
import { startVitest } from 'vitest/node';
|
|
34
37
|
import materialPreviewPlugin from '@forgeax/engine-preview/material';
|
|
35
38
|
import meshPreviewPlugin from '@forgeax/engine-preview/mesh';
|
|
@@ -301,6 +304,534 @@ var init_dist = __esm({
|
|
|
301
304
|
"src/dist.ts"() {
|
|
302
305
|
}
|
|
303
306
|
});
|
|
307
|
+
function projectError(code, expected, hint, detail) {
|
|
308
|
+
return { ok: false, error: { code, expected, hint, detail } };
|
|
309
|
+
}
|
|
310
|
+
async function readJson(path) {
|
|
311
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
312
|
+
}
|
|
313
|
+
function firstUnsupportedStandaloneRealm(entries2, inheritedRealm = "engine") {
|
|
314
|
+
for (const entry of entries2) {
|
|
315
|
+
const realm = entry.realm ?? inheritedRealm ?? "engine";
|
|
316
|
+
if (realm !== "engine") return { id: entry.id, realm };
|
|
317
|
+
if (entry.group === true) {
|
|
318
|
+
const unsupported = firstUnsupportedStandaloneRealm(
|
|
319
|
+
entry.config,
|
|
320
|
+
realm
|
|
321
|
+
);
|
|
322
|
+
if (unsupported !== void 0) return unsupported;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return void 0;
|
|
326
|
+
}
|
|
327
|
+
function pluginModuleNames(entries2) {
|
|
328
|
+
return entries2.flatMap(
|
|
329
|
+
(entry) => entry.group === true ? pluginModuleNames(entry.config) : [entry.name]
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
async function readProjectFacts(rootInput = process.cwd()) {
|
|
333
|
+
const root = resolve(rootInput);
|
|
334
|
+
let forgeValue;
|
|
335
|
+
let packageValue;
|
|
336
|
+
try {
|
|
337
|
+
[forgeValue, packageValue] = await Promise.all([
|
|
338
|
+
readJson(resolve(root, "forge.json")),
|
|
339
|
+
readJson(resolve(root, "package.json"))
|
|
340
|
+
]);
|
|
341
|
+
} catch (cause) {
|
|
342
|
+
return projectError(
|
|
343
|
+
"project-manifest-unreadable",
|
|
344
|
+
"readable forge.json and package.json files",
|
|
345
|
+
"Run the command from a ForgeaX game root or pass its directory.",
|
|
346
|
+
{ root, reason: cause instanceof Error ? cause.message : String(cause) }
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
if (forgeValue === null || typeof forgeValue !== "object") {
|
|
350
|
+
return projectError(
|
|
351
|
+
"project-manifest-invalid",
|
|
352
|
+
"forge.json to contain an object",
|
|
353
|
+
"Repair forge.json before running DevKit.",
|
|
354
|
+
{ root }
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
if (packageValue === null || typeof packageValue !== "object") {
|
|
358
|
+
return projectError(
|
|
359
|
+
"package-manifest-invalid",
|
|
360
|
+
"package.json to contain an object",
|
|
361
|
+
"Repair package.json before running DevKit.",
|
|
362
|
+
{ root }
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
const parsedForge = GameProjectSchema.safeParse(forgeValue);
|
|
366
|
+
if (!parsedForge.success) {
|
|
367
|
+
return projectError(
|
|
368
|
+
"project-manifest-invalid",
|
|
369
|
+
"forge.json to satisfy @forgeax/engine-project GameProjectSchema",
|
|
370
|
+
"Repair the fields reported by the authoritative project schema.",
|
|
371
|
+
{ root, issues: parsedForge.error.issues }
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
const forge = parsedForge.data;
|
|
375
|
+
if (forge.id.length === 0 || forge.name.length === 0 || forge.entry === void 0 || forge.entry.length === 0) {
|
|
376
|
+
return projectError(
|
|
377
|
+
"project-manifest-invalid",
|
|
378
|
+
"forge.json to declare id, name, and entry",
|
|
379
|
+
"Restore the project entry; plugin Entries remain optional additions.",
|
|
380
|
+
{ root }
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
const plugins = forge.plugins ?? [];
|
|
384
|
+
const unsupportedRealm = firstUnsupportedStandaloneRealm(plugins);
|
|
385
|
+
if (unsupportedRealm !== void 0) {
|
|
386
|
+
return projectError(
|
|
387
|
+
"project-plugin-realm-unsupported",
|
|
388
|
+
"the standalone Devkit host to contain only engine-realm plugin Entries",
|
|
389
|
+
"Move Host or build plugins to a host that owns that physical realm.",
|
|
390
|
+
{ root, ...unsupportedRealm }
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
const entryPath = isAbsolute(forge.entry) ? forge.entry : resolve(root, forge.entry);
|
|
394
|
+
try {
|
|
395
|
+
await readFile(entryPath);
|
|
396
|
+
} catch {
|
|
397
|
+
return projectError(
|
|
398
|
+
"project-entry-missing",
|
|
399
|
+
"forge.json#entry to resolve to a readable module",
|
|
400
|
+
"Restore the game entry or update forge.json#entry.",
|
|
401
|
+
{ root, entry: forge.entry }
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
const packageJson = packageValue;
|
|
405
|
+
const forgeax = packageJson.forgeax;
|
|
406
|
+
const configuredRoots = forgeax !== null && typeof forgeax === "object" ? forgeax.assets?.roots : void 0;
|
|
407
|
+
const assetRoots = Array.isArray(configuredRoots) && configuredRoots.every((value) => typeof value === "string") ? configuredRoots : ["assets"];
|
|
408
|
+
const configuredImporters = forgeax !== null && typeof forgeax === "object" ? forgeax.assets?.importers : void 0;
|
|
409
|
+
const assetImporters = Array.isArray(configuredImporters) && configuredImporters.every((value) => typeof value === "string") ? configuredImporters : [];
|
|
410
|
+
const configuredPublicDir = forgeax !== null && typeof forgeax === "object" ? forgeax.assets?.publicDir : void 0;
|
|
411
|
+
const assetPublicDir = typeof configuredPublicDir === "string" ? configuredPublicDir : void 0;
|
|
412
|
+
const physics = forge.physics === "2d" || forge.physics === "3d" ? forge.physics : void 0;
|
|
413
|
+
const defaultScene = typeof forge.defaultScene === "string" && forge.defaultScene.length > 0 ? forge.defaultScene : void 0;
|
|
414
|
+
const normalizedEntry = forge.entry.startsWith("./") ? forge.entry : `./${forge.entry}`;
|
|
415
|
+
const bootstrapEntry = pluginModuleNames(plugins).map((name) => name.startsWith("./") ? name : `./${name}`).includes(normalizedEntry) ? void 0 : forge.entry;
|
|
416
|
+
return {
|
|
417
|
+
ok: true,
|
|
418
|
+
value: {
|
|
419
|
+
root,
|
|
420
|
+
id: forge.id,
|
|
421
|
+
name: forge.name,
|
|
422
|
+
entry: forge.entry,
|
|
423
|
+
...bootstrapEntry === void 0 ? {} : { bootstrapEntry },
|
|
424
|
+
plugins,
|
|
425
|
+
...physics === void 0 ? {} : { physics },
|
|
426
|
+
...defaultScene === void 0 ? {} : { defaultScene },
|
|
427
|
+
assetRoots,
|
|
428
|
+
...assetImporters.length === 0 ? {} : { assetImporters },
|
|
429
|
+
...assetPublicDir === void 0 ? {} : { assetPublicDir },
|
|
430
|
+
packageJson
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
function commandError(cause, fallbackCode) {
|
|
435
|
+
if (cause !== null && typeof cause === "object" && "code" in cause && "expected" in cause && "hint" in cause && "detail" in cause) {
|
|
436
|
+
return cause;
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
code: fallbackCode,
|
|
440
|
+
expected: "the ForgeaX command to complete",
|
|
441
|
+
hint: "Inspect the underlying diagnostic and repair the owning input.",
|
|
442
|
+
detail: { reason: cause instanceof Error ? cause.message : String(cause) }
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
var init_project = __esm({
|
|
446
|
+
"src/project.ts"() {
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
function bindingError(code, expected, hint, detail = {}) {
|
|
450
|
+
return { ok: false, error: { code, expected, hint, detail } };
|
|
451
|
+
}
|
|
452
|
+
function isMissing(cause) {
|
|
453
|
+
return cause !== null && typeof cause === "object" && "code" in cause && cause.code === "ENOENT";
|
|
454
|
+
}
|
|
455
|
+
async function pathExists(path) {
|
|
456
|
+
try {
|
|
457
|
+
await access(path);
|
|
458
|
+
return true;
|
|
459
|
+
} catch {
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function parseBinding(value, path) {
|
|
464
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
465
|
+
return bindingError(
|
|
466
|
+
"engine-binding-invalid",
|
|
467
|
+
"engine-binding.json to contain one object",
|
|
468
|
+
"Run forgeax engine unlink, then select a binding again.",
|
|
469
|
+
{ path }
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
const candidate = value;
|
|
473
|
+
if (candidate.schemaVersion !== ENGINE_BINDING_SCHEMA_VERSION) {
|
|
474
|
+
return bindingError(
|
|
475
|
+
"engine-binding-version-unsupported",
|
|
476
|
+
`engine-binding.json schemaVersion ${ENGINE_BINDING_SCHEMA_VERSION}`,
|
|
477
|
+
"Upgrade the SDK or remove the stale binding with forgeax engine unlink.",
|
|
478
|
+
{ path, schemaVersion: candidate.schemaVersion ?? null }
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
if (typeof candidate.path !== "string" || candidate.path.length === 0) {
|
|
482
|
+
return bindingError(
|
|
483
|
+
"engine-binding-path-missing",
|
|
484
|
+
"engine-binding.json to contain one non-empty local Engine path",
|
|
485
|
+
"Run forgeax engine use-local <engine-directory>.",
|
|
486
|
+
{ path }
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
return {
|
|
490
|
+
ok: true,
|
|
491
|
+
value: {
|
|
492
|
+
schemaVersion: ENGINE_BINDING_SCHEMA_VERSION,
|
|
493
|
+
path: resolve(candidate.path)
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
async function readEngineBinding(rootInput = process.cwd()) {
|
|
498
|
+
const root = resolve(rootInput);
|
|
499
|
+
const path = bindingFile(root);
|
|
500
|
+
try {
|
|
501
|
+
const value = JSON.parse(await readFile(path, "utf8"));
|
|
502
|
+
return parseBinding(value, path);
|
|
503
|
+
} catch (cause) {
|
|
504
|
+
if (isMissing(cause)) return { ok: true, value: null };
|
|
505
|
+
return bindingError(
|
|
506
|
+
"engine-binding-unreadable",
|
|
507
|
+
"engine-binding.json to be readable JSON",
|
|
508
|
+
"Repair or remove .forgeax/engine-binding.json, then select a binding again.",
|
|
509
|
+
{ path, reason: cause instanceof Error ? cause.message : String(cause) }
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
async function writeEngineBinding(root, binding) {
|
|
514
|
+
const path = bindingFile(root);
|
|
515
|
+
const partial = `${path}.partial-${process.pid}`;
|
|
516
|
+
await mkdir(dirname(path), { recursive: true });
|
|
517
|
+
await writeFile(partial, `${JSON.stringify(binding, null, 2)}
|
|
518
|
+
`, "utf8");
|
|
519
|
+
await rename(partial, path);
|
|
520
|
+
}
|
|
521
|
+
function conditionalExport(value) {
|
|
522
|
+
if (typeof value === "string") return value;
|
|
523
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
524
|
+
const record = value;
|
|
525
|
+
for (const key of ["import", "browser", "node", "default", "require", "types"]) {
|
|
526
|
+
const candidate = record[key];
|
|
527
|
+
const selected = conditionalExport(candidate);
|
|
528
|
+
if (selected !== void 0) return selected;
|
|
529
|
+
}
|
|
530
|
+
return void 0;
|
|
531
|
+
}
|
|
532
|
+
function packageEntry(manifest) {
|
|
533
|
+
const exportsValue = manifest.exports;
|
|
534
|
+
if (exportsValue !== void 0) {
|
|
535
|
+
if (typeof exportsValue === "object" && exportsValue !== null && !Array.isArray(exportsValue)) {
|
|
536
|
+
const root = exportsValue["."];
|
|
537
|
+
const selected2 = conditionalExport(root ?? exportsValue);
|
|
538
|
+
if (selected2 !== void 0) return selected2;
|
|
539
|
+
}
|
|
540
|
+
const selected = conditionalExport(exportsValue);
|
|
541
|
+
if (selected !== void 0) return selected;
|
|
542
|
+
}
|
|
543
|
+
return typeof manifest.module === "string" ? manifest.module : typeof manifest.main === "string" ? manifest.main : void 0;
|
|
544
|
+
}
|
|
545
|
+
async function readManifest(path) {
|
|
546
|
+
const value = JSON.parse(await readFile(path, "utf8"));
|
|
547
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
548
|
+
throw new Error(`engine-package-manifest-invalid: ${path}`);
|
|
549
|
+
}
|
|
550
|
+
return value;
|
|
551
|
+
}
|
|
552
|
+
async function inspectEngineWorkspace(workspaceInput) {
|
|
553
|
+
const root = resolve(workspaceInput);
|
|
554
|
+
const packageRoot = resolve(root, "packages");
|
|
555
|
+
try {
|
|
556
|
+
await access(resolve(root, "pnpm-workspace.yaml"));
|
|
557
|
+
const entries2 = await readdir(packageRoot, { withFileTypes: true });
|
|
558
|
+
const packages = [];
|
|
559
|
+
for (const entry of entries2) {
|
|
560
|
+
if (!entry.isDirectory()) continue;
|
|
561
|
+
const packagePath = resolve(packageRoot, entry.name);
|
|
562
|
+
let manifest;
|
|
563
|
+
try {
|
|
564
|
+
manifest = await readManifest(resolve(packagePath, "package.json"));
|
|
565
|
+
} catch {
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
if (typeof manifest.name !== "string" || !manifest.name.startsWith("@forgeax/engine"))
|
|
569
|
+
continue;
|
|
570
|
+
if (typeof manifest.version !== "string") {
|
|
571
|
+
return bindingError(
|
|
572
|
+
"engine-package-version-missing",
|
|
573
|
+
"every local Engine package to declare a version",
|
|
574
|
+
"Restore the package manifest version before selecting a local Engine.",
|
|
575
|
+
{ package: manifest.name, root: packagePath }
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
const target = packageEntry(manifest);
|
|
579
|
+
const entryPath = target === void 0 ? void 0 : resolve(packagePath, target);
|
|
580
|
+
let entryDigest = null;
|
|
581
|
+
let builtAt2 = null;
|
|
582
|
+
if (entryPath !== void 0 && await pathExists(entryPath)) {
|
|
583
|
+
const [bytes, metadata] = await Promise.all([readFile(entryPath), stat(entryPath)]);
|
|
584
|
+
entryDigest = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
585
|
+
builtAt2 = metadata.mtime.toISOString();
|
|
586
|
+
}
|
|
587
|
+
packages.push({
|
|
588
|
+
name: manifest.name,
|
|
589
|
+
root: packagePath,
|
|
590
|
+
version: manifest.version,
|
|
591
|
+
entry: entryPath ?? null,
|
|
592
|
+
built: entryDigest !== null,
|
|
593
|
+
entryDigest,
|
|
594
|
+
builtAt: builtAt2
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
packages.sort((a, b) => a.name.localeCompare(b.name));
|
|
598
|
+
if (packages.length === 0) {
|
|
599
|
+
return bindingError(
|
|
600
|
+
"engine-workspace-empty",
|
|
601
|
+
"the local Engine workspace to contain @forgeax/engine packages",
|
|
602
|
+
"Pass the Engine repository or its source/engine SDK directory.",
|
|
603
|
+
{ root, packageRoot }
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
const versions = [...new Set(packages.map((item) => item.version))].sort();
|
|
607
|
+
const missingBuilds = packages.filter((item) => !item.built).map((item) => item.name);
|
|
608
|
+
const builtAt = packages.flatMap((item) => item.builtAt === null ? [] : [item.builtAt]).sort().at(-1) ?? null;
|
|
609
|
+
const digest = `sha256:${createHash("sha256").update(
|
|
610
|
+
packages.map((item) => `${item.name}\0${item.version}\0${item.entryDigest ?? "unbuilt"}`).join("\n")
|
|
611
|
+
).digest("hex")}`;
|
|
612
|
+
return {
|
|
613
|
+
ok: true,
|
|
614
|
+
value: {
|
|
615
|
+
root,
|
|
616
|
+
packageCount: packages.length,
|
|
617
|
+
builtPackages: packages.length - missingBuilds.length,
|
|
618
|
+
missingBuilds,
|
|
619
|
+
versions,
|
|
620
|
+
packages,
|
|
621
|
+
digest,
|
|
622
|
+
builtAt
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
} catch (cause) {
|
|
626
|
+
return bindingError(
|
|
627
|
+
isMissing(cause) ? "engine-workspace-missing" : "engine-workspace-unreadable",
|
|
628
|
+
"a readable Engine workspace with pnpm-workspace.yaml and packages/",
|
|
629
|
+
"Pass the Engine repository root, not its packages/ directory.",
|
|
630
|
+
{ root, packageRoot, reason: cause instanceof Error ? cause.message : String(cause) }
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
function projectDependencyMode(packageJson) {
|
|
635
|
+
let workspace = false;
|
|
636
|
+
let registry = false;
|
|
637
|
+
for (const section of [
|
|
638
|
+
"dependencies",
|
|
639
|
+
"devDependencies",
|
|
640
|
+
"optionalDependencies",
|
|
641
|
+
"peerDependencies"
|
|
642
|
+
]) {
|
|
643
|
+
const value = packageJson[section];
|
|
644
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) continue;
|
|
645
|
+
for (const version of Object.values(value)) {
|
|
646
|
+
if (typeof version !== "string") continue;
|
|
647
|
+
if (version.startsWith("workspace:") || version.startsWith("file:")) workspace = true;
|
|
648
|
+
else registry = true;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return workspace ? registry ? "mixed" : "workspace" : registry ? "registry" : "none";
|
|
652
|
+
}
|
|
653
|
+
async function sdkResolution(root) {
|
|
654
|
+
const packageJson = resolve(root, "node_modules", "@forgeax", "engine", "package.json");
|
|
655
|
+
try {
|
|
656
|
+
const manifest = await readManifest(packageJson);
|
|
657
|
+
const target = packageEntry(manifest);
|
|
658
|
+
const entry = target === void 0 ? null : resolve(dirname(packageJson), target);
|
|
659
|
+
return {
|
|
660
|
+
root: dirname(packageJson),
|
|
661
|
+
version: typeof manifest.version === "string" ? manifest.version : null,
|
|
662
|
+
entry,
|
|
663
|
+
built: entry !== null && await pathExists(entry),
|
|
664
|
+
source: "sdk"
|
|
665
|
+
};
|
|
666
|
+
} catch {
|
|
667
|
+
return { root: null, version: null, entry: null, built: false, source: "unresolved" };
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
async function engineStatusCommand(options = {}) {
|
|
671
|
+
const facts = await readProjectFacts(options.root);
|
|
672
|
+
if (!facts.ok) return facts;
|
|
673
|
+
const bindingResult = await readEngineBinding(facts.value.root);
|
|
674
|
+
if (!bindingResult.ok) return bindingResult;
|
|
675
|
+
const binding = bindingResult.value;
|
|
676
|
+
const mode = binding === null ? "sdk" : "local";
|
|
677
|
+
const projectDependencies = projectDependencyMode(facts.value.packageJson);
|
|
678
|
+
if (binding !== null) {
|
|
679
|
+
const localPath = binding.path;
|
|
680
|
+
const localBinding = {
|
|
681
|
+
schemaVersion: ENGINE_BINDING_SCHEMA_VERSION,
|
|
682
|
+
path: localPath
|
|
683
|
+
};
|
|
684
|
+
const workspaceResult = await inspectEngineWorkspace(localPath);
|
|
685
|
+
if (!workspaceResult.ok) {
|
|
686
|
+
const resolved3 = {
|
|
687
|
+
root: localPath,
|
|
688
|
+
version: null,
|
|
689
|
+
entry: null,
|
|
690
|
+
built: false,
|
|
691
|
+
source: "local"
|
|
692
|
+
};
|
|
693
|
+
return {
|
|
694
|
+
ok: true,
|
|
695
|
+
value: {
|
|
696
|
+
schemaVersion: ENGINE_BINDING_SCHEMA_VERSION,
|
|
697
|
+
root: facts.value.root,
|
|
698
|
+
binding: localBinding,
|
|
699
|
+
mode,
|
|
700
|
+
resolved: resolved3,
|
|
701
|
+
workspace: null,
|
|
702
|
+
projectDependencies,
|
|
703
|
+
npmCompatible: projectDependencies === "registry" || projectDependencies === "none",
|
|
704
|
+
healthy: false,
|
|
705
|
+
next: ["forgeax engine unlink", "forgeax engine use-local <engine-directory>"],
|
|
706
|
+
diagnostic: workspaceResult.error
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
const workspace = workspaceResult.value;
|
|
711
|
+
const version = workspace.versions.length === 1 ? workspace.versions[0] ?? null : null;
|
|
712
|
+
const umbrella = workspace.packages.find((item) => item.name === "@forgeax/engine");
|
|
713
|
+
const resolved2 = {
|
|
714
|
+
root: localPath,
|
|
715
|
+
version,
|
|
716
|
+
entry: umbrella?.entry ?? null,
|
|
717
|
+
built: umbrella?.built ?? false,
|
|
718
|
+
source: "local"
|
|
719
|
+
};
|
|
720
|
+
const healthy = workspace.versions.length === 1 && workspace.missingBuilds.length === 0;
|
|
721
|
+
return {
|
|
722
|
+
ok: true,
|
|
723
|
+
value: {
|
|
724
|
+
schemaVersion: ENGINE_BINDING_SCHEMA_VERSION,
|
|
725
|
+
root: facts.value.root,
|
|
726
|
+
binding: localBinding,
|
|
727
|
+
mode,
|
|
728
|
+
resolved: resolved2,
|
|
729
|
+
workspace,
|
|
730
|
+
projectDependencies,
|
|
731
|
+
npmCompatible: projectDependencies === "registry" || projectDependencies === "none",
|
|
732
|
+
healthy,
|
|
733
|
+
next: healthy ? ["forgeax build", "forgeax capture --backend auto --require-ui"] : ["pnpm build:engine", "forgeax engine doctor"]
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
const resolved = await sdkResolution(facts.value.root);
|
|
738
|
+
return {
|
|
739
|
+
ok: true,
|
|
740
|
+
value: {
|
|
741
|
+
schemaVersion: ENGINE_BINDING_SCHEMA_VERSION,
|
|
742
|
+
root: facts.value.root,
|
|
743
|
+
binding,
|
|
744
|
+
mode,
|
|
745
|
+
resolved,
|
|
746
|
+
workspace: null,
|
|
747
|
+
projectDependencies,
|
|
748
|
+
npmCompatible: projectDependencies === "registry" || projectDependencies === "none",
|
|
749
|
+
healthy: resolved.source === "sdk" && resolved.built,
|
|
750
|
+
next: resolved.source === "sdk" && resolved.built ? ["forgeax build", "forgeax capture --backend auto --require-ui"] : ["pnpm install", "forgeax doctor"]
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
async function engineUseLocalCommand(options) {
|
|
755
|
+
const root = resolve(options.root ?? process.cwd());
|
|
756
|
+
if (options.path === void 0 || options.path.trim().length === 0) {
|
|
757
|
+
return bindingError(
|
|
758
|
+
"engine-binding-path-missing",
|
|
759
|
+
"forgeax engine use-local <engine-directory>",
|
|
760
|
+
"Pass the local Engine repository or SDK source directory."
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
let localPath;
|
|
764
|
+
try {
|
|
765
|
+
localPath = await realpath(resolve(root, options.path));
|
|
766
|
+
} catch (cause) {
|
|
767
|
+
return bindingError(
|
|
768
|
+
"engine-workspace-missing",
|
|
769
|
+
"the local Engine directory to exist",
|
|
770
|
+
"Pass an existing Engine repository or SDK source directory.",
|
|
771
|
+
{
|
|
772
|
+
path: resolve(root, options.path),
|
|
773
|
+
reason: cause instanceof Error ? cause.message : String(cause)
|
|
774
|
+
}
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
const workspace = await inspectEngineWorkspace(localPath);
|
|
778
|
+
if (!workspace.ok) return workspace;
|
|
779
|
+
const binding = {
|
|
780
|
+
schemaVersion: ENGINE_BINDING_SCHEMA_VERSION,
|
|
781
|
+
path: localPath
|
|
782
|
+
};
|
|
783
|
+
if (options.dryRun !== true) await writeEngineBinding(root, binding);
|
|
784
|
+
const status = await engineStatusCommand({ root });
|
|
785
|
+
if (!status.ok) return status;
|
|
786
|
+
return {
|
|
787
|
+
ok: true,
|
|
788
|
+
value: {
|
|
789
|
+
...status.value,
|
|
790
|
+
next: workspace.value.missingBuilds.length === 0 ? ["forgeax build", "forgeax capture --backend auto --require-ui"] : ["pnpm build:engine", "forgeax engine doctor"]
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
async function engineUnlinkCommand(options = {}) {
|
|
795
|
+
const root = resolve(options.root ?? process.cwd());
|
|
796
|
+
if (options.dryRun !== true) await rm(bindingFile(root), { force: true });
|
|
797
|
+
return engineStatusCommand({ root });
|
|
798
|
+
}
|
|
799
|
+
async function engineDoctorCommand(options = {}) {
|
|
800
|
+
const status = await engineStatusCommand(options);
|
|
801
|
+
if (!status.ok) return status;
|
|
802
|
+
if (status.value.projectDependencies === "workspace" || status.value.projectDependencies === "mixed") {
|
|
803
|
+
return {
|
|
804
|
+
ok: false,
|
|
805
|
+
error: {
|
|
806
|
+
code: "engine-project-workspace-dependency",
|
|
807
|
+
expected: "the game project to use registry or SDK-resolved dependencies for npm consumers",
|
|
808
|
+
hint: "This project is a pnpm workspace; use pnpm here, or create a clean SDK game project for npm install.",
|
|
809
|
+
detail: { root: status.value.root, projectDependencies: status.value.projectDependencies }
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
if (status.value.diagnostic !== void 0) return { ok: false, error: status.value.diagnostic };
|
|
814
|
+
if (!status.value.healthy) {
|
|
815
|
+
return {
|
|
816
|
+
ok: false,
|
|
817
|
+
error: {
|
|
818
|
+
code: status.value.mode === "local" ? "engine-local-build-missing" : "engine-sdk-unresolved",
|
|
819
|
+
expected: "the selected Engine binding to resolve to built packages",
|
|
820
|
+
hint: status.value.mode === "local" ? "Run pnpm build:engine in the selected Engine checkout, then rerun forgeax engine doctor." : "Run pnpm install in the game project or select a valid local Engine checkout.",
|
|
821
|
+
detail: { status: status.value }
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
return status;
|
|
826
|
+
}
|
|
827
|
+
var ENGINE_BINDING_SCHEMA_VERSION, bindingFile;
|
|
828
|
+
var init_engine_binding = __esm({
|
|
829
|
+
"src/engine-binding.ts"() {
|
|
830
|
+
init_project();
|
|
831
|
+
ENGINE_BINDING_SCHEMA_VERSION = "1.0.0";
|
|
832
|
+
bindingFile = (root) => resolve(root, ".forgeax", "engine-binding.json");
|
|
833
|
+
}
|
|
834
|
+
});
|
|
304
835
|
|
|
305
836
|
// src/host.ts
|
|
306
837
|
var host_exports = {};
|
|
@@ -370,40 +901,33 @@ function findEngineWorkspaceRoot() {
|
|
|
370
901
|
cursor = parent;
|
|
371
902
|
}
|
|
372
903
|
}
|
|
373
|
-
async function engineWorkspacePackages() {
|
|
374
|
-
if (
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
if (!
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
);
|
|
388
|
-
if (manifest === null || typeof manifest !== "object") continue;
|
|
389
|
-
const name = manifest.name;
|
|
390
|
-
if (typeof name === "string" && name.startsWith("@forgeax/engine-")) {
|
|
391
|
-
packages.set(name, { root, manifest });
|
|
392
|
-
}
|
|
393
|
-
} catch {
|
|
904
|
+
async function engineWorkspacePackages(workspaceRoot = findEngineWorkspaceRoot()) {
|
|
905
|
+
if (workspaceRoot === void 0) return /* @__PURE__ */ new Map();
|
|
906
|
+
const packageRoot = resolve(workspaceRoot, "packages");
|
|
907
|
+
if (!existsSync(packageRoot)) return /* @__PURE__ */ new Map();
|
|
908
|
+
const packages = /* @__PURE__ */ new Map();
|
|
909
|
+
for (const entry of await readdir(packageRoot, { withFileTypes: true })) {
|
|
910
|
+
if (!entry.isDirectory()) continue;
|
|
911
|
+
const root = resolve(packageRoot, entry.name);
|
|
912
|
+
try {
|
|
913
|
+
const manifest = JSON.parse(await readFile(resolve(root, "package.json"), "utf8"));
|
|
914
|
+
if (manifest === null || typeof manifest !== "object") continue;
|
|
915
|
+
const name = manifest.name;
|
|
916
|
+
if (typeof name === "string" && name.startsWith("@forgeax/engine")) {
|
|
917
|
+
packages.set(name, { root, manifest });
|
|
394
918
|
}
|
|
919
|
+
} catch {
|
|
395
920
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
return engineWorkspacePackagesPromise;
|
|
921
|
+
}
|
|
922
|
+
return packages;
|
|
399
923
|
}
|
|
400
|
-
function
|
|
924
|
+
function conditionalExport2(value) {
|
|
401
925
|
if (typeof value === "string") return value;
|
|
402
926
|
if (value === null || Array.isArray(value)) return void 0;
|
|
403
927
|
for (const condition of ["browser", "import", "node", "default"]) {
|
|
404
928
|
const candidate = value[condition];
|
|
405
929
|
if (candidate === void 0) continue;
|
|
406
|
-
const selected =
|
|
930
|
+
const selected = conditionalExport2(candidate);
|
|
407
931
|
if (selected !== void 0) return selected;
|
|
408
932
|
}
|
|
409
933
|
return void 0;
|
|
@@ -416,14 +940,14 @@ function packageExportTarget(manifest, subpath) {
|
|
|
416
940
|
return typeof main === "string" ? main : void 0;
|
|
417
941
|
}
|
|
418
942
|
if (typeof exportsValue === "string" || exportsValue === null) {
|
|
419
|
-
return subpath.length === 0 ?
|
|
943
|
+
return subpath.length === 0 ? conditionalExport2(exportsValue) : void 0;
|
|
420
944
|
}
|
|
421
945
|
const keys = Object.keys(exportsValue);
|
|
422
946
|
const subpathMap = keys.some((key) => key === "." || key.startsWith("./"));
|
|
423
|
-
if (!subpathMap) return subpath.length === 0 ?
|
|
947
|
+
if (!subpathMap) return subpath.length === 0 ? conditionalExport2(exportsValue) : void 0;
|
|
424
948
|
const requested = subpath.length === 0 ? "." : `./${subpath}`;
|
|
425
949
|
const exact = exportsValue[requested];
|
|
426
|
-
if (exact !== void 0) return
|
|
950
|
+
if (exact !== void 0) return conditionalExport2(exact);
|
|
427
951
|
for (const key of keys) {
|
|
428
952
|
const marker = key.indexOf("*");
|
|
429
953
|
if (marker < 0) continue;
|
|
@@ -433,14 +957,14 @@ function packageExportTarget(manifest, subpath) {
|
|
|
433
957
|
const replacement = requested.slice(prefix.length, requested.length - suffix.length);
|
|
434
958
|
const exportTarget = exportsValue[key];
|
|
435
959
|
if (exportTarget === void 0) continue;
|
|
436
|
-
const selected =
|
|
960
|
+
const selected = conditionalExport2(exportTarget);
|
|
437
961
|
return selected?.replaceAll("*", replacement);
|
|
438
962
|
}
|
|
439
963
|
return void 0;
|
|
440
964
|
}
|
|
441
965
|
function engineWorkspaceImport(source, packages) {
|
|
442
|
-
if (!source.startsWith("@forgeax/engine
|
|
443
|
-
const separator = source.indexOf("/", "@forgeax/engine
|
|
966
|
+
if (!source.startsWith("@forgeax/engine")) return void 0;
|
|
967
|
+
const separator = source.indexOf("/", "@forgeax/engine".length);
|
|
444
968
|
const packageName = separator < 0 ? source : source.slice(0, separator);
|
|
445
969
|
const subpath = separator < 0 ? "" : source.slice(separator + 1);
|
|
446
970
|
const packageInfo = packages.get(packageName);
|
|
@@ -454,14 +978,25 @@ function engineWorkspaceImport(source, packages) {
|
|
|
454
978
|
}
|
|
455
979
|
return absolute;
|
|
456
980
|
}
|
|
457
|
-
async function createEngineWorkspaceResolver() {
|
|
458
|
-
const
|
|
981
|
+
async function createEngineWorkspaceResolver(projectRoot2) {
|
|
982
|
+
const binding = await readEngineBinding(projectRoot2);
|
|
983
|
+
if (!binding.ok) {
|
|
984
|
+
throw new Error(`${binding.error.code}: ${binding.error.hint}`);
|
|
985
|
+
}
|
|
986
|
+
const localRoot = binding.value?.path;
|
|
987
|
+
if (localRoot !== void 0) {
|
|
988
|
+
const inspected = await inspectEngineWorkspace(localRoot);
|
|
989
|
+
if (!inspected.ok) throw new Error(`${inspected.error.code}: ${inspected.error.hint}`);
|
|
990
|
+
}
|
|
991
|
+
const packages = await engineWorkspacePackages(localRoot);
|
|
459
992
|
if (packages.size === 0) return void 0;
|
|
460
993
|
return {
|
|
461
994
|
name: "forgeax:devkit-engine-workspace-resolver",
|
|
995
|
+
enforce: "pre",
|
|
462
996
|
async resolveId(source, importer) {
|
|
463
997
|
const bareSource = source.split("?", 1)[0] ?? source;
|
|
464
|
-
if (!bareSource.startsWith("@forgeax/engine
|
|
998
|
+
if (!bareSource.startsWith("@forgeax/engine")) return null;
|
|
999
|
+
if (localRoot !== void 0) return engineWorkspaceImport(bareSource, packages);
|
|
465
1000
|
try {
|
|
466
1001
|
const resolved = await this.resolve(source, importer, { skipSelf: true });
|
|
467
1002
|
if (resolved !== null) return resolved;
|
|
@@ -713,7 +1248,11 @@ function prepareAssetRegistry(assets) {
|
|
|
713
1248
|
const existing = assetPreparation.get(assets);
|
|
714
1249
|
if (existing !== undefined) return existing;
|
|
715
1250
|
const pending = (async () => {
|
|
716
|
-
|
|
1251
|
+
if (import.meta.env.DEV) {
|
|
1252
|
+
assets.configureRuntimeBinding(runtimeScopeBinding);
|
|
1253
|
+
} else {
|
|
1254
|
+
assets.configurePackIndex(new URL('pack-index.json', document.baseURI).href);
|
|
1255
|
+
}
|
|
717
1256
|
assets.setCatalogSource(assetCatalog);
|
|
718
1257
|
if (vfxRuntimeHost !== undefined) {
|
|
719
1258
|
assets.installDecoder(vfxGpuEffectContribution.kind, vfxGpuEffectContribution.decoder);
|
|
@@ -1166,32 +1705,62 @@ function htmlSource(title) {
|
|
|
1166
1705
|
<div id="app-shell"><canvas id="app"></canvas><div id="game-ui"></div></div><div id="forgeax-fatal" role="alert"></div>
|
|
1167
1706
|
<script>
|
|
1168
1707
|
(() => {
|
|
1708
|
+
const appendStructuredFailure = (value, prefix, depth, seen, lines) => {
|
|
1709
|
+
if (value === null || typeof value !== 'object' || depth > 3) return;
|
|
1710
|
+
if (seen.has(value)) {
|
|
1711
|
+
lines.push((prefix || 'cause') + ': [circular]');
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
seen.add(value);
|
|
1715
|
+
const record = value;
|
|
1716
|
+
const name = typeof record.name === 'string' && record.name.length > 0
|
|
1717
|
+
? record.name
|
|
1718
|
+
: undefined;
|
|
1719
|
+
const code = typeof record.code === 'string' && record.code.length > 0
|
|
1720
|
+
? record.code
|
|
1721
|
+
: undefined;
|
|
1722
|
+
const message = typeof record.message === 'string' && record.message.length > 0
|
|
1723
|
+
? record.message
|
|
1724
|
+
: undefined;
|
|
1725
|
+
if (name !== undefined || code !== undefined || message !== undefined) {
|
|
1726
|
+
const identity = [name || 'Error', code].filter(Boolean).join(' ');
|
|
1727
|
+
lines.push((prefix ? prefix + ': ' : '') + identity + (message ? ': ' + message : ''));
|
|
1728
|
+
}
|
|
1729
|
+
for (const key of ['expected', 'hint', 'reason']) {
|
|
1730
|
+
if (typeof record[key] === 'string' && record[key].length > 0) {
|
|
1731
|
+
lines.push((prefix ? prefix + '.' : '') + key + ': ' + record[key]);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
for (const key of ['cause', 'detail', 'webgpuError', 'wgpuError', 'error']) {
|
|
1735
|
+
const nested = record[key];
|
|
1736
|
+
const nestedPrefix = (prefix ? prefix + '.' : '') + key;
|
|
1737
|
+
if (nested !== null && typeof nested === 'object') {
|
|
1738
|
+
appendStructuredFailure(nested, nestedPrefix, depth + 1, seen, lines);
|
|
1739
|
+
} else if (typeof nested === 'string' && nested.length > 0) {
|
|
1740
|
+
lines.push(nestedPrefix + ': ' + nested);
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
};
|
|
1169
1744
|
const formatStartupFailure = (reason) => {
|
|
1170
|
-
if (reason instanceof Error) return reason.message;
|
|
1171
1745
|
if (reason !== null && typeof reason === 'object') {
|
|
1172
|
-
const record = reason;
|
|
1173
1746
|
const lines = [];
|
|
1174
|
-
|
|
1175
|
-
if (typeof record[key] === 'string' && record[key].length > 0) {
|
|
1176
|
-
lines.push(key + ': ' + record[key]);
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
const detail = record.detail;
|
|
1180
|
-
if (detail !== null && typeof detail === 'object') {
|
|
1181
|
-
for (const key of ['reason', 'guid']) {
|
|
1182
|
-
if (typeof detail[key] === 'string' && detail[key].length > 0) {
|
|
1183
|
-
lines.push('detail.' + key + ': ' + detail[key]);
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1747
|
+
appendStructuredFailure(reason, '', 0, new Set(), lines);
|
|
1187
1748
|
if (lines.length > 0) return lines.join('\\n');
|
|
1749
|
+
try {
|
|
1750
|
+
return JSON.stringify(reason) || 'Unknown structured startup failure';
|
|
1751
|
+
} catch {
|
|
1752
|
+
return 'Unserializable structured startup failure';
|
|
1753
|
+
}
|
|
1188
1754
|
}
|
|
1189
1755
|
return String(reason ?? 'Unknown startup failure');
|
|
1190
1756
|
};
|
|
1191
1757
|
const show = (reason) => {
|
|
1192
1758
|
const notice = document.querySelector('#forgeax-fatal');
|
|
1193
1759
|
if (!(notice instanceof HTMLElement)) return;
|
|
1194
|
-
|
|
1760
|
+
let message = formatStartupFailure(reason);
|
|
1761
|
+
if (/webgpu|adapter-unavailable|no usable (rendering )?backend/i.test(message)) {
|
|
1762
|
+
message += '\\n\\nRenderer diagnosis: ForgeaX supports browser WebGPU and a wgpu/WebGL2 fallback. This failure alone does not prove that WebGPU is unsupported; use the structured code, hint, and nested backend causes above.';
|
|
1763
|
+
}
|
|
1195
1764
|
notice.textContent = 'ForgeaX game failed to start.\\n' + message;
|
|
1196
1765
|
notice.style.display = 'grid';
|
|
1197
1766
|
};
|
|
@@ -1223,7 +1792,7 @@ async function createViteConfig(facts, command2, base = "/", options = {}) {
|
|
|
1223
1792
|
];
|
|
1224
1793
|
const importers = [...projectImporters(facts)];
|
|
1225
1794
|
const runtimeBinding = createStandaloneRuntimeAssetBinding(facts.id);
|
|
1226
|
-
const engineWorkspaceResolver = await createEngineWorkspaceResolver();
|
|
1795
|
+
const engineWorkspaceResolver = await createEngineWorkspaceResolver(facts.root);
|
|
1227
1796
|
const consumerAliases = await consumerEngineAliases(facts.root);
|
|
1228
1797
|
const plugins = [
|
|
1229
1798
|
...engineWorkspaceResolver === void 0 ? [] : [engineWorkspaceResolver],
|
|
@@ -1236,180 +1805,39 @@ async function createViteConfig(facts, command2, base = "/", options = {}) {
|
|
|
1236
1805
|
importers: [
|
|
1237
1806
|
audioImporter,
|
|
1238
1807
|
imageImporter,
|
|
1239
|
-
fbxImporter,
|
|
1240
|
-
gltfImporter,
|
|
1241
|
-
fontImporter,
|
|
1242
|
-
...importers
|
|
1243
|
-
],
|
|
1244
|
-
cookers: [createMaterialPackCooker(roots), createParticleCodeNativeCookerFromRoots(roots)],
|
|
1245
|
-
ignorePath
|
|
1246
|
-
})
|
|
1247
|
-
];
|
|
1248
|
-
return {
|
|
1249
|
-
root: generated,
|
|
1250
|
-
base,
|
|
1251
|
-
configFile: false,
|
|
1252
|
-
publicDir: facts.assetPublicDir === void 0 ? false : resolve(facts.root, facts.assetPublicDir),
|
|
1253
|
-
plugins,
|
|
1254
|
-
resolve: {
|
|
1255
|
-
alias: consumerAliases,
|
|
1256
|
-
dedupe: ["@forgeax/engine"]
|
|
1257
|
-
},
|
|
1258
|
-
server: { ...options.server, fs: { allow: [facts.root, ...roots] } },
|
|
1259
|
-
build: {
|
|
1260
|
-
target: "esnext",
|
|
1261
|
-
outDir: options.outDir ?? resolve(facts.root, "dist"),
|
|
1262
|
-
emptyOutDir: true,
|
|
1263
|
-
rollupOptions: { input: resolve(generated, "index.html") }
|
|
1264
|
-
}
|
|
1265
|
-
};
|
|
1266
|
-
}
|
|
1267
|
-
var hostRequire, engineWorkspacePackagesPromise;
|
|
1268
|
-
var init_host = __esm({
|
|
1269
|
-
"src/host.ts"() {
|
|
1270
|
-
hostRequire = createRequire(import.meta.url);
|
|
1271
|
-
}
|
|
1272
|
-
});
|
|
1273
|
-
function projectError(code, expected, hint, detail) {
|
|
1274
|
-
return { ok: false, error: { code, expected, hint, detail } };
|
|
1275
|
-
}
|
|
1276
|
-
async function readJson(path) {
|
|
1277
|
-
return JSON.parse(await readFile(path, "utf8"));
|
|
1278
|
-
}
|
|
1279
|
-
function firstUnsupportedStandaloneRealm(entries2, inheritedRealm = "engine") {
|
|
1280
|
-
for (const entry of entries2) {
|
|
1281
|
-
const realm = entry.realm ?? inheritedRealm ?? "engine";
|
|
1282
|
-
if (realm !== "engine") return { id: entry.id, realm };
|
|
1283
|
-
if (entry.group === true) {
|
|
1284
|
-
const unsupported = firstUnsupportedStandaloneRealm(
|
|
1285
|
-
entry.config,
|
|
1286
|
-
realm
|
|
1287
|
-
);
|
|
1288
|
-
if (unsupported !== void 0) return unsupported;
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
return void 0;
|
|
1292
|
-
}
|
|
1293
|
-
function pluginModuleNames(entries2) {
|
|
1294
|
-
return entries2.flatMap(
|
|
1295
|
-
(entry) => entry.group === true ? pluginModuleNames(entry.config) : [entry.name]
|
|
1296
|
-
);
|
|
1297
|
-
}
|
|
1298
|
-
async function readProjectFacts(rootInput = process.cwd()) {
|
|
1299
|
-
const root = resolve(rootInput);
|
|
1300
|
-
let forgeValue;
|
|
1301
|
-
let packageValue;
|
|
1302
|
-
try {
|
|
1303
|
-
[forgeValue, packageValue] = await Promise.all([
|
|
1304
|
-
readJson(resolve(root, "forge.json")),
|
|
1305
|
-
readJson(resolve(root, "package.json"))
|
|
1306
|
-
]);
|
|
1307
|
-
} catch (cause) {
|
|
1308
|
-
return projectError(
|
|
1309
|
-
"project-manifest-unreadable",
|
|
1310
|
-
"readable forge.json and package.json files",
|
|
1311
|
-
"Run the command from a ForgeaX game root or pass its directory.",
|
|
1312
|
-
{ root, reason: cause instanceof Error ? cause.message : String(cause) }
|
|
1313
|
-
);
|
|
1314
|
-
}
|
|
1315
|
-
if (forgeValue === null || typeof forgeValue !== "object") {
|
|
1316
|
-
return projectError(
|
|
1317
|
-
"project-manifest-invalid",
|
|
1318
|
-
"forge.json to contain an object",
|
|
1319
|
-
"Repair forge.json before running DevKit.",
|
|
1320
|
-
{ root }
|
|
1321
|
-
);
|
|
1322
|
-
}
|
|
1323
|
-
if (packageValue === null || typeof packageValue !== "object") {
|
|
1324
|
-
return projectError(
|
|
1325
|
-
"package-manifest-invalid",
|
|
1326
|
-
"package.json to contain an object",
|
|
1327
|
-
"Repair package.json before running DevKit.",
|
|
1328
|
-
{ root }
|
|
1329
|
-
);
|
|
1330
|
-
}
|
|
1331
|
-
const parsedForge = GameProjectSchema.safeParse(forgeValue);
|
|
1332
|
-
if (!parsedForge.success) {
|
|
1333
|
-
return projectError(
|
|
1334
|
-
"project-manifest-invalid",
|
|
1335
|
-
"forge.json to satisfy @forgeax/engine-project GameProjectSchema",
|
|
1336
|
-
"Repair the fields reported by the authoritative project schema.",
|
|
1337
|
-
{ root, issues: parsedForge.error.issues }
|
|
1338
|
-
);
|
|
1339
|
-
}
|
|
1340
|
-
const forge = parsedForge.data;
|
|
1341
|
-
if (forge.id.length === 0 || forge.name.length === 0 || forge.entry === void 0 || forge.entry.length === 0) {
|
|
1342
|
-
return projectError(
|
|
1343
|
-
"project-manifest-invalid",
|
|
1344
|
-
"forge.json to declare id, name, and entry",
|
|
1345
|
-
"Restore the project entry; plugin Entries remain optional additions.",
|
|
1346
|
-
{ root }
|
|
1347
|
-
);
|
|
1348
|
-
}
|
|
1349
|
-
const plugins = forge.plugins ?? [];
|
|
1350
|
-
const unsupportedRealm = firstUnsupportedStandaloneRealm(plugins);
|
|
1351
|
-
if (unsupportedRealm !== void 0) {
|
|
1352
|
-
return projectError(
|
|
1353
|
-
"project-plugin-realm-unsupported",
|
|
1354
|
-
"the standalone Devkit host to contain only engine-realm plugin Entries",
|
|
1355
|
-
"Move Host or build plugins to a host that owns that physical realm.",
|
|
1356
|
-
{ root, ...unsupportedRealm }
|
|
1357
|
-
);
|
|
1358
|
-
}
|
|
1359
|
-
const entryPath = isAbsolute(forge.entry) ? forge.entry : resolve(root, forge.entry);
|
|
1360
|
-
try {
|
|
1361
|
-
await readFile(entryPath);
|
|
1362
|
-
} catch {
|
|
1363
|
-
return projectError(
|
|
1364
|
-
"project-entry-missing",
|
|
1365
|
-
"forge.json#entry to resolve to a readable module",
|
|
1366
|
-
"Restore the game entry or update forge.json#entry.",
|
|
1367
|
-
{ root, entry: forge.entry }
|
|
1368
|
-
);
|
|
1369
|
-
}
|
|
1370
|
-
const packageJson = packageValue;
|
|
1371
|
-
const forgeax = packageJson.forgeax;
|
|
1372
|
-
const configuredRoots = forgeax !== null && typeof forgeax === "object" ? forgeax.assets?.roots : void 0;
|
|
1373
|
-
const assetRoots = Array.isArray(configuredRoots) && configuredRoots.every((value) => typeof value === "string") ? configuredRoots : ["assets"];
|
|
1374
|
-
const configuredImporters = forgeax !== null && typeof forgeax === "object" ? forgeax.assets?.importers : void 0;
|
|
1375
|
-
const assetImporters = Array.isArray(configuredImporters) && configuredImporters.every((value) => typeof value === "string") ? configuredImporters : [];
|
|
1376
|
-
const configuredPublicDir = forgeax !== null && typeof forgeax === "object" ? forgeax.assets?.publicDir : void 0;
|
|
1377
|
-
const assetPublicDir = typeof configuredPublicDir === "string" ? configuredPublicDir : void 0;
|
|
1378
|
-
const physics = forge.physics === "2d" || forge.physics === "3d" ? forge.physics : void 0;
|
|
1379
|
-
const defaultScene = typeof forge.defaultScene === "string" && forge.defaultScene.length > 0 ? forge.defaultScene : void 0;
|
|
1380
|
-
const normalizedEntry = forge.entry.startsWith("./") ? forge.entry : `./${forge.entry}`;
|
|
1381
|
-
const bootstrapEntry = pluginModuleNames(plugins).map((name) => name.startsWith("./") ? name : `./${name}`).includes(normalizedEntry) ? void 0 : forge.entry;
|
|
1382
|
-
return {
|
|
1383
|
-
ok: true,
|
|
1384
|
-
value: {
|
|
1385
|
-
root,
|
|
1386
|
-
id: forge.id,
|
|
1387
|
-
name: forge.name,
|
|
1388
|
-
entry: forge.entry,
|
|
1389
|
-
...bootstrapEntry === void 0 ? {} : { bootstrapEntry },
|
|
1390
|
-
plugins,
|
|
1391
|
-
...physics === void 0 ? {} : { physics },
|
|
1392
|
-
...defaultScene === void 0 ? {} : { defaultScene },
|
|
1393
|
-
assetRoots,
|
|
1394
|
-
...assetImporters.length === 0 ? {} : { assetImporters },
|
|
1395
|
-
...assetPublicDir === void 0 ? {} : { assetPublicDir },
|
|
1396
|
-
packageJson
|
|
1397
|
-
}
|
|
1398
|
-
};
|
|
1399
|
-
}
|
|
1400
|
-
function commandError(cause, fallbackCode) {
|
|
1401
|
-
if (cause !== null && typeof cause === "object" && "code" in cause && "expected" in cause && "hint" in cause && "detail" in cause) {
|
|
1402
|
-
return cause;
|
|
1403
|
-
}
|
|
1808
|
+
fbxImporter,
|
|
1809
|
+
gltfImporter,
|
|
1810
|
+
fontImporter,
|
|
1811
|
+
...importers
|
|
1812
|
+
],
|
|
1813
|
+
cookers: [createMaterialPackCooker(roots), createParticleCodeNativeCookerFromRoots(roots)],
|
|
1814
|
+
ignorePath
|
|
1815
|
+
})
|
|
1816
|
+
];
|
|
1404
1817
|
return {
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1818
|
+
root: generated,
|
|
1819
|
+
base,
|
|
1820
|
+
configFile: false,
|
|
1821
|
+
publicDir: facts.assetPublicDir === void 0 ? false : resolve(facts.root, facts.assetPublicDir),
|
|
1822
|
+
plugins,
|
|
1823
|
+
resolve: {
|
|
1824
|
+
alias: consumerAliases,
|
|
1825
|
+
dedupe: ["@forgeax/engine"]
|
|
1826
|
+
},
|
|
1827
|
+
server: { ...options.server, fs: { allow: [facts.root, ...roots] } },
|
|
1828
|
+
build: {
|
|
1829
|
+
target: "esnext",
|
|
1830
|
+
outDir: options.outDir ?? resolve(facts.root, "dist"),
|
|
1831
|
+
emptyOutDir: true,
|
|
1832
|
+
rollupOptions: { input: resolve(generated, "index.html") }
|
|
1833
|
+
}
|
|
1409
1834
|
};
|
|
1410
1835
|
}
|
|
1411
|
-
var
|
|
1412
|
-
|
|
1836
|
+
var hostRequire;
|
|
1837
|
+
var init_host = __esm({
|
|
1838
|
+
"src/host.ts"() {
|
|
1839
|
+
init_engine_binding();
|
|
1840
|
+
hostRequire = createRequire(import.meta.url);
|
|
1413
1841
|
}
|
|
1414
1842
|
});
|
|
1415
1843
|
|
|
@@ -1679,6 +2107,7 @@ var init_package = __esm({
|
|
|
1679
2107
|
"@forgeax/engine-project": "workspace:*",
|
|
1680
2108
|
"@forgeax/engine-rhi-debug": "workspace:*",
|
|
1681
2109
|
"@forgeax/engine-rhi-null": "workspace:*",
|
|
2110
|
+
"@forgeax/engine-rhi-webgpu": "workspace:*",
|
|
1682
2111
|
"@forgeax/engine-render": "workspace:*",
|
|
1683
2112
|
"@forgeax/engine-render-graph": "workspace:*",
|
|
1684
2113
|
"@forgeax/engine-runtime": "workspace:*",
|
|
@@ -1693,7 +2122,8 @@ var init_package = __esm({
|
|
|
1693
2122
|
jiti: "1.21.7",
|
|
1694
2123
|
playwright: "1.60.0",
|
|
1695
2124
|
vite: "8.0.10",
|
|
1696
|
-
vitest: "4.
|
|
2125
|
+
vitest: "4.0.18",
|
|
2126
|
+
webgpu: "^0.4.0"
|
|
1697
2127
|
},
|
|
1698
2128
|
devDependencies: {
|
|
1699
2129
|
"@types/node": "^20.14.0"
|
|
@@ -1867,7 +2297,7 @@ var init_init = __esm({
|
|
|
1867
2297
|
"@webgpu/types": "0.1.71",
|
|
1868
2298
|
tsx: "4.23.1",
|
|
1869
2299
|
typescript: "6.0.3",
|
|
1870
|
-
vitest: "4.
|
|
2300
|
+
vitest: "4.0.18"
|
|
1871
2301
|
};
|
|
1872
2302
|
}
|
|
1873
2303
|
});
|
|
@@ -1934,6 +2364,7 @@ function sdkProjectInstallArgs(store) {
|
|
|
1934
2364
|
"install",
|
|
1935
2365
|
"--frozen-lockfile",
|
|
1936
2366
|
"--ignore-scripts",
|
|
2367
|
+
"--config.pm-on-fail=ignore",
|
|
1937
2368
|
"--side-effects-cache=true",
|
|
1938
2369
|
"--child-concurrency=1"
|
|
1939
2370
|
];
|
|
@@ -1943,6 +2374,7 @@ function sdkBootstrapInstallArgs(store) {
|
|
|
1943
2374
|
const common = [
|
|
1944
2375
|
"install",
|
|
1945
2376
|
"--frozen-lockfile",
|
|
2377
|
+
"--config.pm-on-fail=ignore",
|
|
1946
2378
|
"--child-concurrency=1",
|
|
1947
2379
|
"--side-effects-cache=true"
|
|
1948
2380
|
];
|
|
@@ -2854,7 +3286,7 @@ __export(plugin_authoring_exports, {
|
|
|
2854
3286
|
pluginInstallCommand: () => pluginInstallCommand,
|
|
2855
3287
|
pluginUninstallCommand: () => pluginUninstallCommand
|
|
2856
3288
|
});
|
|
2857
|
-
async function
|
|
3289
|
+
async function readManifest2(root) {
|
|
2858
3290
|
const path = resolve(root, "forge.json");
|
|
2859
3291
|
try {
|
|
2860
3292
|
const raw = await readFile(path, "utf8");
|
|
@@ -2907,7 +3339,7 @@ async function mutateDependency(root, action, dependency) {
|
|
|
2907
3339
|
}
|
|
2908
3340
|
async function pluginInstallCommand(options) {
|
|
2909
3341
|
const root = resolve(options.root ?? process.cwd());
|
|
2910
|
-
const manifest = await
|
|
3342
|
+
const manifest = await readManifest2(root);
|
|
2911
3343
|
if (!manifest.ok) return manifest;
|
|
2912
3344
|
const parsed = GameProjectSchema.safeParse(manifest.value.value);
|
|
2913
3345
|
if (!parsed.success) {
|
|
@@ -2960,7 +3392,7 @@ async function pluginInstallCommand(options) {
|
|
|
2960
3392
|
}
|
|
2961
3393
|
async function pluginUninstallCommand(options) {
|
|
2962
3394
|
const root = resolve(options.root ?? process.cwd());
|
|
2963
|
-
const manifest = await
|
|
3395
|
+
const manifest = await readManifest2(root);
|
|
2964
3396
|
if (!manifest.ok) return manifest;
|
|
2965
3397
|
const parsed = GameProjectSchema.safeParse(manifest.value.value);
|
|
2966
3398
|
if (!parsed.success) {
|
|
@@ -3048,26 +3480,62 @@ function createCliRhiDebugOperationContext() {
|
|
|
3048
3480
|
};
|
|
3049
3481
|
}
|
|
3050
3482
|
},
|
|
3051
|
-
async createReplayBackend() {
|
|
3483
|
+
async createReplayBackend(tape) {
|
|
3484
|
+
let createDawn;
|
|
3485
|
+
let gpuGlobals;
|
|
3486
|
+
try {
|
|
3487
|
+
const dawn = await import('webgpu');
|
|
3488
|
+
createDawn = dawn.create;
|
|
3489
|
+
gpuGlobals = dawn.globals;
|
|
3490
|
+
} catch (cause) {
|
|
3491
|
+
return {
|
|
3492
|
+
ok: false,
|
|
3493
|
+
error: {
|
|
3494
|
+
code: "replay-backend-unavailable",
|
|
3495
|
+
expected: "the Dawn WebGPU provider to load",
|
|
3496
|
+
hint: "Install the DevKit runtime closure and retry rhi.inspect.",
|
|
3497
|
+
detail: {
|
|
3498
|
+
stage: "provider",
|
|
3499
|
+
cause: cause instanceof Error ? cause.message : String(cause)
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
};
|
|
3503
|
+
}
|
|
3504
|
+
Object.assign(globalThis, gpuGlobals);
|
|
3505
|
+
const gpu = createDawn([]);
|
|
3506
|
+
if (!("navigator" in globalThis) || globalThis.navigator === void 0) {
|
|
3507
|
+
Object.defineProperty(globalThis, "navigator", {
|
|
3508
|
+
value: {},
|
|
3509
|
+
configurable: true,
|
|
3510
|
+
writable: true
|
|
3511
|
+
});
|
|
3512
|
+
}
|
|
3513
|
+
Object.defineProperty(globalThis.navigator, "gpu", {
|
|
3514
|
+
value: gpu,
|
|
3515
|
+
configurable: true,
|
|
3516
|
+
writable: true
|
|
3517
|
+
});
|
|
3052
3518
|
const adapter = await rhi.requestAdapter();
|
|
3053
3519
|
if (!adapter.ok) {
|
|
3054
3520
|
return {
|
|
3055
3521
|
ok: false,
|
|
3056
3522
|
error: {
|
|
3057
3523
|
code: "replay-backend-unavailable",
|
|
3058
|
-
expected: "a fresh
|
|
3524
|
+
expected: "a fresh Dawn WebGPU adapter",
|
|
3059
3525
|
hint: adapter.error.hint,
|
|
3060
3526
|
detail: { stage: "adapter", cause: adapter.error.hint }
|
|
3061
3527
|
}
|
|
3062
3528
|
};
|
|
3063
3529
|
}
|
|
3064
|
-
const device = await adapter.value.requestDevice(
|
|
3530
|
+
const device = await adapter.value.requestDevice(
|
|
3531
|
+
replayDeviceRequest(tape, adapter.value.features, adapter.value.limits)
|
|
3532
|
+
);
|
|
3065
3533
|
if (!device.ok) {
|
|
3066
3534
|
return {
|
|
3067
3535
|
ok: false,
|
|
3068
3536
|
error: {
|
|
3069
3537
|
code: "replay-backend-unavailable",
|
|
3070
|
-
expected: "a fresh
|
|
3538
|
+
expected: "a fresh Dawn WebGPU device satisfying the recorded tape",
|
|
3071
3539
|
hint: device.error.hint,
|
|
3072
3540
|
detail: { stage: "device", cause: device.error.hint }
|
|
3073
3541
|
}
|
|
@@ -3105,6 +3573,9 @@ function renderRhiDebugHelp() {
|
|
|
3105
3573
|
return [
|
|
3106
3574
|
"forgeax run <operation>",
|
|
3107
3575
|
...discoverRhiDebugOperations().map((operation) => ` ${operation.name}: ${operation.summary}`),
|
|
3576
|
+
"Usage:",
|
|
3577
|
+
" forgeax run rhi.summary --artifact PATH --digest SHA256 --json",
|
|
3578
|
+
" forgeax run rhi.inspect --artifact PATH --digest SHA256 --work-index N --fields pipeline,bindings,pixels --json",
|
|
3108
3579
|
"ArtifactRef schema:",
|
|
3109
3580
|
JSON.stringify(RHI_DEBUG_OPERATION_MANIFEST.artifactRefSchema)
|
|
3110
3581
|
].join("\n");
|
|
@@ -3232,7 +3703,7 @@ async function runRhiDebugOperation(name, input, context) {
|
|
|
3232
3703
|
"Provide a fresh device and shader factory before running rhi.inspect."
|
|
3233
3704
|
);
|
|
3234
3705
|
}
|
|
3235
|
-
const backend = await context.createReplayBackend();
|
|
3706
|
+
const backend = await context.createReplayBackend(decoded.value);
|
|
3236
3707
|
if (!backend.ok) return backend;
|
|
3237
3708
|
const opened = await openReplay(decoded.value, backend.value);
|
|
3238
3709
|
if (!opened.ok) return { ok: false, error: coreError(opened.error) };
|
|
@@ -3324,7 +3795,10 @@ var init_operations = __esm({
|
|
|
3324
3795
|
properties: {
|
|
3325
3796
|
artifact: artifactRefSchema,
|
|
3326
3797
|
workIndex: { type: "integer", minimum: 0 },
|
|
3327
|
-
fields: {
|
|
3798
|
+
fields: {
|
|
3799
|
+
type: "array",
|
|
3800
|
+
items: { type: "string", enum: ["bindings", "pipeline", "pixels"] }
|
|
3801
|
+
}
|
|
3328
3802
|
},
|
|
3329
3803
|
required: ["artifact", "workIndex"],
|
|
3330
3804
|
additionalProperties: false
|
|
@@ -3473,6 +3947,630 @@ var init_shader_check = __esm({
|
|
|
3473
3947
|
init_project();
|
|
3474
3948
|
}
|
|
3475
3949
|
});
|
|
3950
|
+
function fail(code, expected, hint, detail = {}) {
|
|
3951
|
+
throw new SoftwareCaptureError(code, expected, hint, detail);
|
|
3952
|
+
}
|
|
3953
|
+
async function pathExists2(path) {
|
|
3954
|
+
try {
|
|
3955
|
+
await access(path);
|
|
3956
|
+
return true;
|
|
3957
|
+
} catch {
|
|
3958
|
+
return false;
|
|
3959
|
+
}
|
|
3960
|
+
}
|
|
3961
|
+
async function reservePort() {
|
|
3962
|
+
const reservation = createServer();
|
|
3963
|
+
await new Promise((resolveListen, rejectListen) => {
|
|
3964
|
+
reservation.once("error", rejectListen);
|
|
3965
|
+
reservation.listen(0, "127.0.0.1", resolveListen);
|
|
3966
|
+
});
|
|
3967
|
+
const address = reservation.address();
|
|
3968
|
+
const port = typeof address === "object" && address !== null ? address.port : 0;
|
|
3969
|
+
await new Promise((resolveClose, rejectClose) => {
|
|
3970
|
+
reservation.close((error) => error === void 0 ? resolveClose() : rejectClose(error));
|
|
3971
|
+
});
|
|
3972
|
+
if (port === 0) throw new Error("OS did not assign an ephemeral capture port");
|
|
3973
|
+
return port;
|
|
3974
|
+
}
|
|
3975
|
+
async function stopProcess(child) {
|
|
3976
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
3977
|
+
await new Promise((resolveStop) => {
|
|
3978
|
+
const timeout = setTimeout(() => {
|
|
3979
|
+
child.kill("SIGKILL");
|
|
3980
|
+
resolveStop();
|
|
3981
|
+
}, 2e3);
|
|
3982
|
+
child.once("exit", () => {
|
|
3983
|
+
clearTimeout(timeout);
|
|
3984
|
+
resolveStop();
|
|
3985
|
+
});
|
|
3986
|
+
child.kill("SIGTERM");
|
|
3987
|
+
});
|
|
3988
|
+
}
|
|
3989
|
+
async function startVirtualDisplay(width, height) {
|
|
3990
|
+
if (process.platform !== "linux" || process.env.DISPLAY !== void 0) {
|
|
3991
|
+
return {
|
|
3992
|
+
...process.env.DISPLAY === void 0 ? {} : { value: process.env.DISPLAY },
|
|
3993
|
+
async close() {
|
|
3994
|
+
}
|
|
3995
|
+
};
|
|
3996
|
+
}
|
|
3997
|
+
for (let offset = 0; offset < 100; offset += 1) {
|
|
3998
|
+
const number = 90 + (process.pid + offset) % 100;
|
|
3999
|
+
const socket = `/tmp/.X11-unix/X${number}`;
|
|
4000
|
+
const lock = `/tmp/.X${number}-lock`;
|
|
4001
|
+
if (await pathExists2(socket) || await pathExists2(lock)) continue;
|
|
4002
|
+
const display = `:${number}`;
|
|
4003
|
+
const child = spawn(
|
|
4004
|
+
"Xvfb",
|
|
4005
|
+
[display, "-screen", "0", `${width}x${height}x24`, "-nolisten", "tcp"],
|
|
4006
|
+
{ stdio: ["ignore", "ignore", "pipe"] }
|
|
4007
|
+
);
|
|
4008
|
+
let diagnostic = "";
|
|
4009
|
+
child.stderr?.setEncoding("utf8");
|
|
4010
|
+
child.stderr?.on("data", (chunk) => {
|
|
4011
|
+
diagnostic += chunk;
|
|
4012
|
+
});
|
|
4013
|
+
try {
|
|
4014
|
+
await new Promise((resolveReady, rejectReady) => {
|
|
4015
|
+
const timeout = setTimeout(
|
|
4016
|
+
() => rejectReady(new Error(`Xvfb did not create ${socket}: ${diagnostic.trim()}`)),
|
|
4017
|
+
5e3
|
|
4018
|
+
);
|
|
4019
|
+
const poll = setInterval(() => {
|
|
4020
|
+
void pathExists2(socket).then((exists) => {
|
|
4021
|
+
if (!exists) return;
|
|
4022
|
+
clearTimeout(timeout);
|
|
4023
|
+
clearInterval(poll);
|
|
4024
|
+
resolveReady();
|
|
4025
|
+
});
|
|
4026
|
+
}, 50);
|
|
4027
|
+
child.once("error", (error) => {
|
|
4028
|
+
clearTimeout(timeout);
|
|
4029
|
+
clearInterval(poll);
|
|
4030
|
+
rejectReady(error);
|
|
4031
|
+
});
|
|
4032
|
+
child.once("exit", (code, signal) => {
|
|
4033
|
+
clearTimeout(timeout);
|
|
4034
|
+
clearInterval(poll);
|
|
4035
|
+
rejectReady(
|
|
4036
|
+
new Error(`Xvfb exited before ready (${code ?? signal}): ${diagnostic.trim()}`)
|
|
4037
|
+
);
|
|
4038
|
+
});
|
|
4039
|
+
});
|
|
4040
|
+
return { value: display, close: () => stopProcess(child) };
|
|
4041
|
+
} catch (cause) {
|
|
4042
|
+
await stopProcess(child);
|
|
4043
|
+
throw cause;
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
4046
|
+
throw new Error("no free X11 display number was available for browser capture");
|
|
4047
|
+
}
|
|
4048
|
+
async function lavapipeIcd() {
|
|
4049
|
+
for (const candidate of [
|
|
4050
|
+
"/usr/share/vulkan/icd.d/lvp_icd.x86_64.json",
|
|
4051
|
+
"/usr/share/vulkan/icd.d/lvp_icd.aarch64.json"
|
|
4052
|
+
]) {
|
|
4053
|
+
if (await pathExists2(candidate)) return candidate;
|
|
4054
|
+
}
|
|
4055
|
+
return void 0;
|
|
4056
|
+
}
|
|
4057
|
+
function evidencePath(output) {
|
|
4058
|
+
return output.toLowerCase().endsWith(".png") ? `${output.slice(0, -4)}.json` : `${output}.json`;
|
|
4059
|
+
}
|
|
4060
|
+
function newRunId() {
|
|
4061
|
+
return `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
|
|
4062
|
+
}
|
|
4063
|
+
function checkpointSlug(checkpoint) {
|
|
4064
|
+
const value = (checkpoint ?? "capture").trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
|
|
4065
|
+
return value.length === 0 ? "capture" : value;
|
|
4066
|
+
}
|
|
4067
|
+
function captureDigest(bytes) {
|
|
4068
|
+
return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
4069
|
+
}
|
|
4070
|
+
function observedBackend(runtime) {
|
|
4071
|
+
if (runtime.adapter === null) return "unknown";
|
|
4072
|
+
return isSoftwareAdapter(runtime) ? "software" : "hardware";
|
|
4073
|
+
}
|
|
4074
|
+
async function resolveBrowserExecutable(requested) {
|
|
4075
|
+
const candidates = [
|
|
4076
|
+
requested,
|
|
4077
|
+
process.env.FORGEAX_BROWSER_EXECUTABLE,
|
|
4078
|
+
"/opt/google/chrome-beta/chrome",
|
|
4079
|
+
"/usr/bin/google-chrome",
|
|
4080
|
+
"/usr/bin/google-chrome-stable",
|
|
4081
|
+
"/usr/bin/chromium",
|
|
4082
|
+
"/usr/bin/chromium-browser"
|
|
4083
|
+
].filter((candidate) => candidate !== void 0 && candidate.length > 0);
|
|
4084
|
+
for (const candidate of candidates) {
|
|
4085
|
+
if (await pathExists2(candidate)) return candidate;
|
|
4086
|
+
}
|
|
4087
|
+
return void 0;
|
|
4088
|
+
}
|
|
4089
|
+
function browserLaunchArgs(backend) {
|
|
4090
|
+
const common = [
|
|
4091
|
+
"--enable-unsafe-webgpu",
|
|
4092
|
+
"--ignore-gpu-blocklist",
|
|
4093
|
+
"--disable-gpu-driver-bug-workarounds",
|
|
4094
|
+
"--force-color-profile=srgb",
|
|
4095
|
+
"--force-device-scale-factor=1"
|
|
4096
|
+
];
|
|
4097
|
+
if (backend !== "software") return common;
|
|
4098
|
+
return [
|
|
4099
|
+
...common,
|
|
4100
|
+
"--enable-features=Vulkan",
|
|
4101
|
+
"--use-vulkan=swiftshader",
|
|
4102
|
+
"--use-angle=swiftshader",
|
|
4103
|
+
"--disable-vulkan-surface"
|
|
4104
|
+
];
|
|
4105
|
+
}
|
|
4106
|
+
function summarizeCapturePixels(rgba, width, height) {
|
|
4107
|
+
const pixelCount = width * height;
|
|
4108
|
+
const stride = Math.max(1, Math.floor(pixelCount / 4096));
|
|
4109
|
+
const histogram = new Uint32Array(16);
|
|
4110
|
+
let lumaMin = 255;
|
|
4111
|
+
let lumaMax = 0;
|
|
4112
|
+
let sampledPixels = 0;
|
|
4113
|
+
for (let pixel = 0; pixel < pixelCount; pixel += stride) {
|
|
4114
|
+
const offset = pixel * 4;
|
|
4115
|
+
const red = rgba[offset];
|
|
4116
|
+
const green = rgba[offset + 1];
|
|
4117
|
+
const blue = rgba[offset + 2];
|
|
4118
|
+
if (red === void 0 || green === void 0 || blue === void 0) break;
|
|
4119
|
+
const luma = Math.round((54 * red + 183 * green + 19 * blue) / 256);
|
|
4120
|
+
lumaMin = Math.min(lumaMin, luma);
|
|
4121
|
+
lumaMax = Math.max(lumaMax, luma);
|
|
4122
|
+
const bucket = Math.min(15, Math.floor(luma / 16));
|
|
4123
|
+
histogram[bucket] = (histogram[bucket] ?? 0) + 1;
|
|
4124
|
+
sampledPixels += 1;
|
|
4125
|
+
}
|
|
4126
|
+
const dominantPixels = histogram.reduce((largest, count) => Math.max(largest, count), 0);
|
|
4127
|
+
const varyingPixels = sampledPixels - dominantPixels;
|
|
4128
|
+
const lumaRange = lumaMax - lumaMin;
|
|
4129
|
+
const requiredVariation = Math.min(sampledPixels, Math.max(8, Math.ceil(sampledPixels * 2e-3)));
|
|
4130
|
+
return {
|
|
4131
|
+
width,
|
|
4132
|
+
height,
|
|
4133
|
+
sampledPixels,
|
|
4134
|
+
lumaMin,
|
|
4135
|
+
lumaMax,
|
|
4136
|
+
lumaRange,
|
|
4137
|
+
varyingPixels,
|
|
4138
|
+
rendered: lumaRange >= 8 && varyingPixels >= requiredVariation
|
|
4139
|
+
};
|
|
4140
|
+
}
|
|
4141
|
+
function inspectCapturePng(png) {
|
|
4142
|
+
const decoded = parseImage(png, "image/png", { mipmap: false });
|
|
4143
|
+
if (!decoded.ok) throw decoded.error;
|
|
4144
|
+
return summarizeCapturePixels(decoded.value.bytes, decoded.value.width, decoded.value.height);
|
|
4145
|
+
}
|
|
4146
|
+
async function screenshotWithWitness(page) {
|
|
4147
|
+
const canvasPng = await page.locator("canvas").first().screenshot({
|
|
4148
|
+
type: "png",
|
|
4149
|
+
style: "* { visibility: hidden !important; } canvas { visibility: visible !important; }"
|
|
4150
|
+
});
|
|
4151
|
+
const png = await page.screenshot({ type: "png", caret: "hide" });
|
|
4152
|
+
return { png, pixels: inspectCapturePng(canvasPng) };
|
|
4153
|
+
}
|
|
4154
|
+
async function waitForCompositor(page) {
|
|
4155
|
+
await page.evaluate(async () => {
|
|
4156
|
+
await document.fonts.ready;
|
|
4157
|
+
await new Promise((resolveFrame) => requestAnimationFrame(() => resolveFrame()));
|
|
4158
|
+
await new Promise((resolveFrame) => requestAnimationFrame(() => resolveFrame()));
|
|
4159
|
+
});
|
|
4160
|
+
}
|
|
4161
|
+
async function runtimeWitness(page) {
|
|
4162
|
+
return page.evaluate(async (datasetKey) => {
|
|
4163
|
+
const canvas = document.querySelector("canvas");
|
|
4164
|
+
const uiRoot = document.querySelector("#game-ui");
|
|
4165
|
+
const shadowHosts = [...uiRoot?.querySelectorAll("*") ?? []].filter(
|
|
4166
|
+
(element) => element.shadowRoot?.childElementCount !== 0
|
|
4167
|
+
);
|
|
4168
|
+
let adapter = null;
|
|
4169
|
+
let adapterError = null;
|
|
4170
|
+
try {
|
|
4171
|
+
const gpuAdapter = await navigator.gpu?.requestAdapter();
|
|
4172
|
+
if (gpuAdapter === null || gpuAdapter === void 0) {
|
|
4173
|
+
adapterError = "navigator.gpu.requestAdapter() returned null";
|
|
4174
|
+
} else {
|
|
4175
|
+
const info = gpuAdapter.info;
|
|
4176
|
+
adapter = {
|
|
4177
|
+
vendor: info.vendor,
|
|
4178
|
+
architecture: info.architecture,
|
|
4179
|
+
device: info.device,
|
|
4180
|
+
description: info.description
|
|
4181
|
+
};
|
|
4182
|
+
}
|
|
4183
|
+
} catch (cause) {
|
|
4184
|
+
adapterError = String(cause);
|
|
4185
|
+
}
|
|
4186
|
+
const frameId = Number(document.documentElement.dataset[datasetKey]);
|
|
4187
|
+
return {
|
|
4188
|
+
title: document.title,
|
|
4189
|
+
canvas: canvas instanceof HTMLCanvasElement ? { width: canvas.width, height: canvas.height } : null,
|
|
4190
|
+
domUi: {
|
|
4191
|
+
rootChildren: uiRoot?.childElementCount ?? 0,
|
|
4192
|
+
openShadowRoots: shadowHosts.length,
|
|
4193
|
+
textWitness: shadowHosts.map((host) => host.shadowRoot?.textContent?.replace(/\s+/g, " ").trim() ?? "").filter((text) => text.length > 0).join(" | ").slice(0, 500)
|
|
4194
|
+
},
|
|
4195
|
+
adapter,
|
|
4196
|
+
adapterError,
|
|
4197
|
+
engineFrameId: Number.isSafeInteger(frameId) && frameId > 0 ? frameId : null,
|
|
4198
|
+
captureReady: document.documentElement.dataset.forgeaxCaptureReady ?? null,
|
|
4199
|
+
userAgent: navigator.userAgent
|
|
4200
|
+
};
|
|
4201
|
+
}, FORGEAX_FRAME_SUBMITTED_DATASET);
|
|
4202
|
+
}
|
|
4203
|
+
function isSoftwareAdapter(runtime) {
|
|
4204
|
+
const witness = Object.values(runtime.adapter ?? {}).map((value) => String(value).toLowerCase()).join(" ");
|
|
4205
|
+
return ["swiftshader", "llvmpipe", "lavapipe", "software"].some(
|
|
4206
|
+
(token) => witness.includes(token)
|
|
4207
|
+
);
|
|
4208
|
+
}
|
|
4209
|
+
async function writeRunReport(report) {
|
|
4210
|
+
await mkdir(dirname(report.report), { recursive: true });
|
|
4211
|
+
await writeFile(report.report, `${JSON.stringify(report, null, 2)}
|
|
4212
|
+
`, "utf8");
|
|
4213
|
+
}
|
|
4214
|
+
async function openBrowserCaptureSession(root, options) {
|
|
4215
|
+
const backend = options.backend ?? (options.software === true ? "software" : "auto");
|
|
4216
|
+
if (options.software === true && options.backend !== void 0 && options.backend !== "software") {
|
|
4217
|
+
fail(
|
|
4218
|
+
"browser-capture-option-conflict",
|
|
4219
|
+
"software and backend options to describe the same capture lane",
|
|
4220
|
+
"Use either software: true or backend: software; use backend: auto for a portable capture.",
|
|
4221
|
+
{ software: options.software, backend: options.backend }
|
|
4222
|
+
);
|
|
4223
|
+
}
|
|
4224
|
+
const facts = await readProjectFacts(root);
|
|
4225
|
+
if (!facts.ok) {
|
|
4226
|
+
throw new SoftwareCaptureError(
|
|
4227
|
+
facts.error.code,
|
|
4228
|
+
facts.error.expected,
|
|
4229
|
+
facts.error.hint,
|
|
4230
|
+
facts.error.detail
|
|
4231
|
+
);
|
|
4232
|
+
}
|
|
4233
|
+
const width = options.width ?? 1280;
|
|
4234
|
+
const height = options.height ?? 720;
|
|
4235
|
+
const browserPath = await resolveBrowserExecutable(options.browser);
|
|
4236
|
+
if (options.browser !== void 0 && browserPath === void 0) {
|
|
4237
|
+
fail(
|
|
4238
|
+
"browser-capture-browser-missing",
|
|
4239
|
+
`a runnable browser at ${options.browser}`,
|
|
4240
|
+
"Install Chrome/Chromium or pass --browser with an executable path.",
|
|
4241
|
+
{ browser: options.browser }
|
|
4242
|
+
);
|
|
4243
|
+
}
|
|
4244
|
+
const id = options.runId ?? newRunId();
|
|
4245
|
+
const outputDirectory = resolve(
|
|
4246
|
+
facts.value.root,
|
|
4247
|
+
options.outputDir ?? `artifacts/playthrough/${id}`
|
|
4248
|
+
);
|
|
4249
|
+
const reportPath = resolve(
|
|
4250
|
+
facts.value.root,
|
|
4251
|
+
options.report ?? resolve(outputDirectory, "run.json")
|
|
4252
|
+
);
|
|
4253
|
+
const port = options.port === void 0 || options.port === 0 ? await reservePort() : options.port;
|
|
4254
|
+
let server;
|
|
4255
|
+
let browser;
|
|
4256
|
+
let page;
|
|
4257
|
+
let display;
|
|
4258
|
+
try {
|
|
4259
|
+
const { createServer: createServer2 } = await import('vite');
|
|
4260
|
+
server = await createServer2(
|
|
4261
|
+
await createViteConfig(facts.value, "serve", "/", {
|
|
4262
|
+
server: { port, strictPort: true }
|
|
4263
|
+
})
|
|
4264
|
+
);
|
|
4265
|
+
await server.listen(port);
|
|
4266
|
+
display = await startVirtualDisplay(width, height);
|
|
4267
|
+
const captureDisplay = display;
|
|
4268
|
+
const icd = await lavapipeIcd();
|
|
4269
|
+
const consoleErrors = [];
|
|
4270
|
+
const pageErrors = [];
|
|
4271
|
+
const hasDisplay = display.value !== void 0;
|
|
4272
|
+
const headless = options.headless ?? (!hasDisplay && process.platform !== "linux");
|
|
4273
|
+
const baseUrl = new URL(server.resolvedUrls?.local[0] ?? `http://127.0.0.1:${port}/`);
|
|
4274
|
+
const openPage = async (launchBackend) => {
|
|
4275
|
+
const browserEnvironment = {
|
|
4276
|
+
...Object.fromEntries(
|
|
4277
|
+
Object.entries(process.env).filter(
|
|
4278
|
+
(entry) => entry[1] !== void 0
|
|
4279
|
+
)
|
|
4280
|
+
),
|
|
4281
|
+
...launchBackend === "software" ? { LIBGL_ALWAYS_SOFTWARE: "1" } : {},
|
|
4282
|
+
...captureDisplay.value === void 0 ? {} : { DISPLAY: captureDisplay.value }
|
|
4283
|
+
};
|
|
4284
|
+
const launchOptions = {
|
|
4285
|
+
headless,
|
|
4286
|
+
env: browserEnvironment,
|
|
4287
|
+
args: browserLaunchArgs(launchBackend),
|
|
4288
|
+
...browserPath === void 0 ? {} : { executablePath: browserPath }
|
|
4289
|
+
};
|
|
4290
|
+
browser = await chromium.launch(launchOptions);
|
|
4291
|
+
page = await browser.newPage({
|
|
4292
|
+
viewport: { width, height },
|
|
4293
|
+
screen: { width, height },
|
|
4294
|
+
deviceScaleFactor: 1,
|
|
4295
|
+
colorScheme: "light",
|
|
4296
|
+
locale: "en-US",
|
|
4297
|
+
timezoneId: "UTC",
|
|
4298
|
+
serviceWorkers: "block"
|
|
4299
|
+
});
|
|
4300
|
+
page.on("console", (message) => {
|
|
4301
|
+
if (message.type() === "error") consoleErrors.push(message.text());
|
|
4302
|
+
});
|
|
4303
|
+
page.on("pageerror", (error) => pageErrors.push(String(error)));
|
|
4304
|
+
const captureUrl2 = new URL(baseUrl.href);
|
|
4305
|
+
if (options.deterministic === true) captureUrl2.searchParams.set("forgeaxCapture", "1");
|
|
4306
|
+
await page.goto(captureUrl2.href, { waitUntil: "domcontentloaded", timeout: 12e4 });
|
|
4307
|
+
await page.waitForFunction(
|
|
4308
|
+
(requireUi) => {
|
|
4309
|
+
const canvas = document.querySelector("canvas");
|
|
4310
|
+
if (!(canvas instanceof HTMLCanvasElement) || canvas.width <= 0 || canvas.height <= 0)
|
|
4311
|
+
return false;
|
|
4312
|
+
if (!requireUi) return true;
|
|
4313
|
+
const uiRoot = document.querySelector("#game-ui");
|
|
4314
|
+
return uiRoot !== null && uiRoot.childElementCount > 0;
|
|
4315
|
+
},
|
|
4316
|
+
options.requireUi === true,
|
|
4317
|
+
{ timeout: 12e4 }
|
|
4318
|
+
);
|
|
4319
|
+
return runtimeWitness(page);
|
|
4320
|
+
};
|
|
4321
|
+
const initialLaunchBackend = backend === "software" ? "software" : "hardware";
|
|
4322
|
+
let runtime = await openPage(initialLaunchBackend);
|
|
4323
|
+
let observed = observedBackend(runtime);
|
|
4324
|
+
if (backend === "auto" && runtime.adapter === null) {
|
|
4325
|
+
await browser?.close();
|
|
4326
|
+
browser = void 0;
|
|
4327
|
+
page = void 0;
|
|
4328
|
+
consoleErrors.length = 0;
|
|
4329
|
+
pageErrors.length = 0;
|
|
4330
|
+
runtime = await openPage("software");
|
|
4331
|
+
observed = observedBackend(runtime);
|
|
4332
|
+
}
|
|
4333
|
+
const captureUrl = new URL(baseUrl.href);
|
|
4334
|
+
if (options.deterministic === true) captureUrl.searchParams.set("forgeaxCapture", "1");
|
|
4335
|
+
if (page === void 0 || browser === void 0) {
|
|
4336
|
+
fail(
|
|
4337
|
+
"browser-capture-browser-unavailable",
|
|
4338
|
+
"the browser capture session to open",
|
|
4339
|
+
"Inspect the browser launch diagnostic and retry with --browser or --headless."
|
|
4340
|
+
);
|
|
4341
|
+
}
|
|
4342
|
+
const captures = [];
|
|
4343
|
+
let closed = false;
|
|
4344
|
+
const report = {
|
|
4345
|
+
schemaVersion: "2.0.0",
|
|
4346
|
+
runId: id,
|
|
4347
|
+
ok: false,
|
|
4348
|
+
mode: "browser-compositor",
|
|
4349
|
+
root: facts.value.root,
|
|
4350
|
+
url: captureUrl.href,
|
|
4351
|
+
report: reportPath,
|
|
4352
|
+
backendRequested: backend,
|
|
4353
|
+
backend: observed,
|
|
4354
|
+
softwareRequested: backend === "software",
|
|
4355
|
+
deterministicRequested: options.deterministic === true,
|
|
4356
|
+
viewport: {
|
|
4357
|
+
width,
|
|
4358
|
+
height,
|
|
4359
|
+
deviceScaleFactor: 1,
|
|
4360
|
+
colorProfile: "srgb",
|
|
4361
|
+
colorScheme: "light",
|
|
4362
|
+
locale: "en-US",
|
|
4363
|
+
timezone: "UTC"
|
|
4364
|
+
},
|
|
4365
|
+
browser: { version: browser.version(), executable: browserPath ?? "playwright-managed" },
|
|
4366
|
+
display: display.value ?? null,
|
|
4367
|
+
lavapipeIcd: icd ?? null,
|
|
4368
|
+
captures,
|
|
4369
|
+
consoleErrors,
|
|
4370
|
+
pageErrors,
|
|
4371
|
+
boundary: "Browser-compositor capture is visual iteration evidence, not physical-GPU performance, HDR-display output, or release acceptance."
|
|
4372
|
+
};
|
|
4373
|
+
await writeRunReport(report);
|
|
4374
|
+
const updateReport = async () => {
|
|
4375
|
+
Object.assign(report, {
|
|
4376
|
+
ok: captures.length > 0 && captures.every((capture) => capture.ok) && consoleErrors.length === 0 && pageErrors.length === 0
|
|
4377
|
+
});
|
|
4378
|
+
await writeRunReport(report);
|
|
4379
|
+
};
|
|
4380
|
+
const close = async () => {
|
|
4381
|
+
if (closed) return;
|
|
4382
|
+
closed = true;
|
|
4383
|
+
await Promise.allSettled([browser?.close(), display?.close(), server?.close()]);
|
|
4384
|
+
Object.assign(report, { closedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
4385
|
+
await updateReport();
|
|
4386
|
+
};
|
|
4387
|
+
return {
|
|
4388
|
+
page,
|
|
4389
|
+
url: captureUrl.href,
|
|
4390
|
+
reportPath,
|
|
4391
|
+
async capture(checkpoint, captureOptions = {}) {
|
|
4392
|
+
if (closed) {
|
|
4393
|
+
fail(
|
|
4394
|
+
"browser-capture-session-closed",
|
|
4395
|
+
"capture to run inside an open browser session",
|
|
4396
|
+
"Open one session, complete all checkpoints, then close it.",
|
|
4397
|
+
{ report: reportPath }
|
|
4398
|
+
);
|
|
4399
|
+
}
|
|
4400
|
+
const activePage = page;
|
|
4401
|
+
if (activePage === void 0) {
|
|
4402
|
+
fail(
|
|
4403
|
+
"browser-capture-browser-unavailable",
|
|
4404
|
+
"an open Playwright page",
|
|
4405
|
+
"Inspect the browser launch diagnostic and retry the capture."
|
|
4406
|
+
);
|
|
4407
|
+
}
|
|
4408
|
+
const expectedReady = checkpoint ?? (options.deterministic === true ? "true" : void 0);
|
|
4409
|
+
await activePage.waitForFunction(
|
|
4410
|
+
({ datasetKey }) => Number(document.documentElement.dataset[datasetKey]) > 0,
|
|
4411
|
+
{ datasetKey: FORGEAX_FRAME_SUBMITTED_DATASET },
|
|
4412
|
+
{ timeout: 12e4 }
|
|
4413
|
+
);
|
|
4414
|
+
if (expectedReady !== void 0) {
|
|
4415
|
+
await activePage.waitForFunction(
|
|
4416
|
+
(expected) => document.documentElement.dataset.forgeaxCaptureReady === expected,
|
|
4417
|
+
expectedReady,
|
|
4418
|
+
{ timeout: 12e4 }
|
|
4419
|
+
);
|
|
4420
|
+
}
|
|
4421
|
+
await waitForCompositor(activePage);
|
|
4422
|
+
let captured = await screenshotWithWitness(activePage);
|
|
4423
|
+
const deadline = Date.now() + 12e4;
|
|
4424
|
+
while (!captured.pixels.rendered && Date.now() < deadline) {
|
|
4425
|
+
await activePage.waitForTimeout(500);
|
|
4426
|
+
await waitForCompositor(activePage);
|
|
4427
|
+
captured = await screenshotWithWitness(activePage);
|
|
4428
|
+
}
|
|
4429
|
+
const waitMs = captureOptions.waitMs ?? 0;
|
|
4430
|
+
if (waitMs > 0) {
|
|
4431
|
+
await activePage.waitForTimeout(waitMs);
|
|
4432
|
+
await waitForCompositor(activePage);
|
|
4433
|
+
captured = await screenshotWithWitness(activePage);
|
|
4434
|
+
while (!captured.pixels.rendered && Date.now() < deadline) {
|
|
4435
|
+
await activePage.waitForTimeout(500);
|
|
4436
|
+
await waitForCompositor(activePage);
|
|
4437
|
+
captured = await screenshotWithWitness(activePage);
|
|
4438
|
+
}
|
|
4439
|
+
}
|
|
4440
|
+
const runtime2 = await runtimeWitness(activePage);
|
|
4441
|
+
const uiPresent = runtime2.domUi.rootChildren > 0;
|
|
4442
|
+
const requireUi = captureOptions.requireUi ?? options.requireUi ?? false;
|
|
4443
|
+
const captureBackend = observedBackend(runtime2);
|
|
4444
|
+
if (captureBackend !== "unknown") Object.assign(report, { backend: captureBackend });
|
|
4445
|
+
const backendMatches = backend === "auto" || captureBackend === backend;
|
|
4446
|
+
const ok2 = runtime2.canvas !== null && captured.pixels.rendered && backendMatches && runtime2.engineFrameId !== null && consoleErrors.length === 0 && pageErrors.length === 0 && (expectedReady === void 0 || runtime2.captureReady === expectedReady) && (!requireUi || uiPresent);
|
|
4447
|
+
const index = captures.length + 1;
|
|
4448
|
+
const output = resolve(
|
|
4449
|
+
facts.value.root,
|
|
4450
|
+
captureOptions.output ?? resolve(
|
|
4451
|
+
outputDirectory,
|
|
4452
|
+
`${String(index).padStart(3, "0")}-${checkpointSlug(checkpoint)}.png`
|
|
4453
|
+
)
|
|
4454
|
+
);
|
|
4455
|
+
await mkdir(dirname(output), { recursive: true });
|
|
4456
|
+
await writeFile(output, captured.png);
|
|
4457
|
+
const record = {
|
|
4458
|
+
index,
|
|
4459
|
+
checkpoint: checkpoint ?? null,
|
|
4460
|
+
ok: ok2,
|
|
4461
|
+
output,
|
|
4462
|
+
digest: captureDigest(captured.png),
|
|
4463
|
+
pixels: captured.pixels,
|
|
4464
|
+
runtime: runtime2
|
|
4465
|
+
};
|
|
4466
|
+
captures.push(record);
|
|
4467
|
+
await updateReport();
|
|
4468
|
+
if (!ok2) {
|
|
4469
|
+
fail(
|
|
4470
|
+
`${backend === "software" ? "software" : "browser"}-capture-runtime-failed`,
|
|
4471
|
+
"non-flat canvas pixels, the requested browser backend/checkpoint, and no browser errors",
|
|
4472
|
+
"Inspect the run report and repair the first browser runtime failure.",
|
|
4473
|
+
{ checkpoint: checkpoint ?? null, output, report: reportPath }
|
|
4474
|
+
);
|
|
4475
|
+
}
|
|
4476
|
+
return record;
|
|
4477
|
+
},
|
|
4478
|
+
report: () => report,
|
|
4479
|
+
close
|
|
4480
|
+
};
|
|
4481
|
+
} catch (cause) {
|
|
4482
|
+
await Promise.allSettled([browser?.close(), display?.close(), server?.close()]);
|
|
4483
|
+
throw cause;
|
|
4484
|
+
}
|
|
4485
|
+
}
|
|
4486
|
+
function createBrowserCapture(root) {
|
|
4487
|
+
const sessions = /* @__PURE__ */ new Set();
|
|
4488
|
+
return {
|
|
4489
|
+
async open(options) {
|
|
4490
|
+
const session = await openBrowserCaptureSession(root, options);
|
|
4491
|
+
sessions.add(session);
|
|
4492
|
+
return session;
|
|
4493
|
+
},
|
|
4494
|
+
async close() {
|
|
4495
|
+
await Promise.allSettled([...sessions].map((session) => session.close()));
|
|
4496
|
+
sessions.clear();
|
|
4497
|
+
}
|
|
4498
|
+
};
|
|
4499
|
+
}
|
|
4500
|
+
function captureCommandError(cause, legacySoftware = false) {
|
|
4501
|
+
if (cause instanceof SoftwareCaptureError) {
|
|
4502
|
+
const code = legacySoftware && cause.code.startsWith("browser-capture-") ? cause.code.replace(/^browser-capture-/, "software-capture-") : cause.code;
|
|
4503
|
+
return {
|
|
4504
|
+
code,
|
|
4505
|
+
expected: cause.expected,
|
|
4506
|
+
hint: cause.hint,
|
|
4507
|
+
detail: cause.detail
|
|
4508
|
+
};
|
|
4509
|
+
}
|
|
4510
|
+
return commandError(cause, legacySoftware ? "software-capture-failed" : "browser-capture-failed");
|
|
4511
|
+
}
|
|
4512
|
+
async function browserCaptureCommand(options) {
|
|
4513
|
+
const root = options.root ?? process.cwd();
|
|
4514
|
+
const output = resolve(root, options.output ?? "artifacts/capture/game-ui.png");
|
|
4515
|
+
const browser = createBrowserCapture(root);
|
|
4516
|
+
try {
|
|
4517
|
+
const backend = options.backend ?? (options.software === true ? "software" : "auto");
|
|
4518
|
+
const session = await browser.open({
|
|
4519
|
+
backend,
|
|
4520
|
+
...backend === "software" ? { software: true } : {},
|
|
4521
|
+
...options.browser === void 0 ? {} : { browser: options.browser },
|
|
4522
|
+
...options.width === void 0 ? {} : { width: options.width },
|
|
4523
|
+
...options.height === void 0 ? {} : { height: options.height },
|
|
4524
|
+
...options.port === void 0 ? {} : { port: options.port },
|
|
4525
|
+
...options.headless === void 0 ? {} : { headless: options.headless },
|
|
4526
|
+
requireUi: options.requireUi === true,
|
|
4527
|
+
deterministic: options.deterministic === true,
|
|
4528
|
+
outputDir: dirname(output),
|
|
4529
|
+
report: evidencePath(output)
|
|
4530
|
+
});
|
|
4531
|
+
await session.capture(options.deterministic === true ? "true" : void 0, {
|
|
4532
|
+
output,
|
|
4533
|
+
waitMs: options.waitMs ?? 4e3,
|
|
4534
|
+
requireUi: options.requireUi === true
|
|
4535
|
+
});
|
|
4536
|
+
await session.close();
|
|
4537
|
+
return { ok: true, value: session.report() };
|
|
4538
|
+
} catch (cause) {
|
|
4539
|
+
return { ok: false, error: captureCommandError(cause) };
|
|
4540
|
+
} finally {
|
|
4541
|
+
await browser.close();
|
|
4542
|
+
}
|
|
4543
|
+
}
|
|
4544
|
+
async function softwareCaptureCommand(options) {
|
|
4545
|
+
const result = await browserCaptureCommand({ ...options, backend: "software", software: true });
|
|
4546
|
+
if (result.ok) return result;
|
|
4547
|
+
return { ok: false, error: captureCommandErrorFromResult(result.error) };
|
|
4548
|
+
}
|
|
4549
|
+
function captureCommandErrorFromResult(error, legacySoftware) {
|
|
4550
|
+
if (!error.code.startsWith("browser-capture-")) return error;
|
|
4551
|
+
return { ...error, code: error.code.replace(/^browser-capture-/, "software-capture-") };
|
|
4552
|
+
}
|
|
4553
|
+
var SoftwareCaptureError;
|
|
4554
|
+
var init_software_capture = __esm({
|
|
4555
|
+
"src/software-capture.ts"() {
|
|
4556
|
+
init_host();
|
|
4557
|
+
init_project();
|
|
4558
|
+
SoftwareCaptureError = class extends Error {
|
|
4559
|
+
constructor(code, expected, hint, detail) {
|
|
4560
|
+
super(`${code}: ${hint}`);
|
|
4561
|
+
this.code = code;
|
|
4562
|
+
this.expected = expected;
|
|
4563
|
+
this.hint = hint;
|
|
4564
|
+
this.detail = detail;
|
|
4565
|
+
this.name = "SoftwareCaptureError";
|
|
4566
|
+
}
|
|
4567
|
+
code;
|
|
4568
|
+
expected;
|
|
4569
|
+
hint;
|
|
4570
|
+
detail;
|
|
4571
|
+
};
|
|
4572
|
+
}
|
|
4573
|
+
});
|
|
3476
4574
|
|
|
3477
4575
|
// src/commands.ts
|
|
3478
4576
|
var commands_exports = {};
|
|
@@ -3482,12 +4580,17 @@ __export(commands_exports, {
|
|
|
3482
4580
|
assetInspectCommand: () => assetInspectCommand,
|
|
3483
4581
|
assetListCommand: () => assetListCommand,
|
|
3484
4582
|
assetVerifyCommand: () => assetVerifyCommand,
|
|
4583
|
+
browserCaptureCommand: () => browserCaptureCommand,
|
|
3485
4584
|
buildCommand: () => buildCommand,
|
|
3486
4585
|
createCliRhiDebugOperationContext: () => createCliRhiDebugOperationContext,
|
|
3487
4586
|
createRhiDebugOperationContext: () => createRhiDebugOperationContext,
|
|
3488
4587
|
devCommand: () => devCommand,
|
|
3489
4588
|
discoverRhiDebugOperations: () => discoverRhiDebugOperations,
|
|
3490
4589
|
doctorCommand: () => doctorCommand,
|
|
4590
|
+
engineDoctorCommand: () => engineDoctorCommand,
|
|
4591
|
+
engineStatusCommand: () => engineStatusCommand,
|
|
4592
|
+
engineUnlinkCommand: () => engineUnlinkCommand,
|
|
4593
|
+
engineUseLocalCommand: () => engineUseLocalCommand,
|
|
3491
4594
|
initCommand: () => initCommand,
|
|
3492
4595
|
newCommand: () => newCommand,
|
|
3493
4596
|
packageCommand: () => packageCommand,
|
|
@@ -3502,6 +4605,7 @@ __export(commands_exports, {
|
|
|
3502
4605
|
shaderCheckCommand: () => shaderCheckCommand,
|
|
3503
4606
|
skillInstallCommand: () => skillInstallCommand,
|
|
3504
4607
|
skillVerifyCommand: () => skillVerifyCommand,
|
|
4608
|
+
softwareCaptureCommand: () => softwareCaptureCommand,
|
|
3505
4609
|
testCommand: () => testCommand
|
|
3506
4610
|
});
|
|
3507
4611
|
function runRhiDebugCommand(name, input, context) {
|
|
@@ -3742,12 +4846,14 @@ var init_commands = __esm({
|
|
|
3742
4846
|
init_types();
|
|
3743
4847
|
init_assets();
|
|
3744
4848
|
init_bootstrap_commands();
|
|
4849
|
+
init_engine_binding();
|
|
3745
4850
|
init_plugin_authoring();
|
|
3746
4851
|
init_cli_context();
|
|
3747
4852
|
init_operations();
|
|
3748
4853
|
init_sdk_install();
|
|
3749
4854
|
init_shader_check();
|
|
3750
4855
|
init_skill_install();
|
|
4856
|
+
init_software_capture();
|
|
3751
4857
|
init_operations();
|
|
3752
4858
|
}
|
|
3753
4859
|
});
|
|
@@ -4396,12 +5502,15 @@ function sdkUpdateLines(value) {
|
|
|
4396
5502
|
];
|
|
4397
5503
|
}
|
|
4398
5504
|
function renderForgeaxUsage() {
|
|
4399
|
-
return "Usage: forgeax new [directory] [--template empty|game-
|
|
5505
|
+
return "Usage: forgeax new [directory] [--template empty|game-3d]\n forgeax <init|doctor|test|dev|build|package|serve|preview> [directory]\n forgeax capture [directory] [--backend auto|software|hardware] [--require-ui] [--deterministic] [--headless] [--output PATH]\n forgeax engine <status|doctor|unlink> [--root directory]\n forgeax engine use-local <engine-directory> [--root directory]\n forgeax package [directory] [--output release/game-web.zip]\n forgeax run <rhi.capture|rhi.summary|rhi.inspect> [options]\n forgeax run <operation-id> --input <request.json>\n forgeax exec <program.mjs> [--json]\n forgeax asset <add|verify|inspect|list> [subject]\n forgeax shader check [path]\n forgeax plugin <install|uninstall> <module-or-id> [options]\n forgeax skill <install|verify> [--root directory]\n forgeax sdk install <directory> [--version VERSION]\n";
|
|
4400
5506
|
}
|
|
4401
5507
|
|
|
4402
5508
|
// src/cli.ts
|
|
4403
5509
|
init_commands();
|
|
4404
5510
|
|
|
5511
|
+
// src/tools/commands.ts
|
|
5512
|
+
init_software_capture();
|
|
5513
|
+
|
|
4405
5514
|
// src/tools/client.ts
|
|
4406
5515
|
init_catalog();
|
|
4407
5516
|
function decorateResourcePreviewTerminal(terminal) {
|
|
@@ -4572,6 +5681,7 @@ async function execCommand(options) {
|
|
|
4572
5681
|
};
|
|
4573
5682
|
}
|
|
4574
5683
|
const root = options.root ?? process.cwd();
|
|
5684
|
+
const browser = createBrowserCapture(root);
|
|
4575
5685
|
try {
|
|
4576
5686
|
const module = await import(pathToFileURL(resolve(root, options.program)).href);
|
|
4577
5687
|
const program = module.default ?? module.run;
|
|
@@ -4579,7 +5689,8 @@ async function execCommand(options) {
|
|
|
4579
5689
|
throw new TypeError("operation program must export a default function or named run function");
|
|
4580
5690
|
}
|
|
4581
5691
|
const client = await createToolClient({ projectRoot: root });
|
|
4582
|
-
const
|
|
5692
|
+
const context = { ...client, browser };
|
|
5693
|
+
const value = await program(context);
|
|
4583
5694
|
if (!isSerializableValue(value)) {
|
|
4584
5695
|
throw new TypeError("tool program returned a non-serializable live value");
|
|
4585
5696
|
}
|
|
@@ -4594,6 +5705,8 @@ async function execCommand(options) {
|
|
|
4594
5705
|
detail: { reason: cause instanceof Error ? cause.message : String(cause) }
|
|
4595
5706
|
}
|
|
4596
5707
|
};
|
|
5708
|
+
} finally {
|
|
5709
|
+
await browser.close();
|
|
4597
5710
|
}
|
|
4598
5711
|
}
|
|
4599
5712
|
|
|
@@ -4602,8 +5715,8 @@ init_types();
|
|
|
4602
5715
|
var rawArgs = process.argv.slice(2);
|
|
4603
5716
|
var args = [...rawArgs];
|
|
4604
5717
|
var primary = args.shift();
|
|
4605
|
-
var nested = primary === "asset" || primary === "shader" || primary === "plugin" || primary === "skill" || primary === "sdk" ? args.shift() : void 0;
|
|
4606
|
-
var command = primary === "asset" || primary === "shader" || primary === "plugin" || primary === "skill" || primary === "sdk" ? `${primary}.${nested ?? ""}` : primary;
|
|
5718
|
+
var nested = primary === "asset" || primary === "shader" || primary === "plugin" || primary === "skill" || primary === "sdk" || primary === "engine" ? args.shift() : void 0;
|
|
5719
|
+
var command = primary === "asset" || primary === "shader" || primary === "plugin" || primary === "skill" || primary === "sdk" || primary === "engine" ? `${primary}.${nested ?? ""}` : primary;
|
|
4607
5720
|
var json = args.includes("--json");
|
|
4608
5721
|
var dryRun = args.includes("--dry-run");
|
|
4609
5722
|
var noInstall = args.includes("--no-install");
|
|
@@ -4616,6 +5729,8 @@ var optionValueIndexes = new Set(
|
|
|
4616
5729
|
[
|
|
4617
5730
|
"--artifact",
|
|
4618
5731
|
"--base",
|
|
5732
|
+
"--backend",
|
|
5733
|
+
"--browser",
|
|
4619
5734
|
"--digest",
|
|
4620
5735
|
"--id",
|
|
4621
5736
|
"--realm",
|
|
@@ -4626,7 +5741,10 @@ var optionValueIndexes = new Set(
|
|
|
4626
5741
|
"--root",
|
|
4627
5742
|
"--port",
|
|
4628
5743
|
"--template",
|
|
5744
|
+
"--height",
|
|
4629
5745
|
"--version",
|
|
5746
|
+
"--wait-ms",
|
|
5747
|
+
"--width",
|
|
4630
5748
|
"--work-index"
|
|
4631
5749
|
].flatMap((name) => {
|
|
4632
5750
|
const index = args.indexOf(name);
|
|
@@ -4646,6 +5764,11 @@ var commands = [
|
|
|
4646
5764
|
"dev",
|
|
4647
5765
|
"build",
|
|
4648
5766
|
"package",
|
|
5767
|
+
"capture",
|
|
5768
|
+
"engine.status",
|
|
5769
|
+
"engine.use-local",
|
|
5770
|
+
"engine.unlink",
|
|
5771
|
+
"engine.doctor",
|
|
4649
5772
|
"serve",
|
|
4650
5773
|
"preview",
|
|
4651
5774
|
"run",
|
|
@@ -4712,6 +5835,92 @@ async function run(value) {
|
|
|
4712
5835
|
...output === void 0 ? {} : { output }
|
|
4713
5836
|
});
|
|
4714
5837
|
}
|
|
5838
|
+
case "capture": {
|
|
5839
|
+
const parsedPort = parseProjectPortOption(option("--port"), args.includes("--port"));
|
|
5840
|
+
if (!parsedPort.ok) return parsedPort;
|
|
5841
|
+
const software = args.includes("--software");
|
|
5842
|
+
const requestedBackend = option("--backend");
|
|
5843
|
+
if (requestedBackend !== void 0 && requestedBackend !== "auto" && requestedBackend !== "software" && requestedBackend !== "hardware") {
|
|
5844
|
+
return {
|
|
5845
|
+
ok: false,
|
|
5846
|
+
error: {
|
|
5847
|
+
code: "cli-parse-error",
|
|
5848
|
+
expected: "--backend to be auto, software, or hardware",
|
|
5849
|
+
hint: "Use auto for a portable capture, or assert one browser rendering lane explicitly.",
|
|
5850
|
+
detail: { option: "--backend", received: requestedBackend }
|
|
5851
|
+
}
|
|
5852
|
+
};
|
|
5853
|
+
}
|
|
5854
|
+
if (software && requestedBackend !== void 0 && requestedBackend !== "software") {
|
|
5855
|
+
return {
|
|
5856
|
+
ok: false,
|
|
5857
|
+
error: {
|
|
5858
|
+
code: "cli-parse-error",
|
|
5859
|
+
expected: "--software and --backend to select the same capture lane",
|
|
5860
|
+
hint: "Use --backend software (or only the legacy --software flag).",
|
|
5861
|
+
detail: { software: true, backend: requestedBackend }
|
|
5862
|
+
}
|
|
5863
|
+
};
|
|
5864
|
+
}
|
|
5865
|
+
const parseInteger = (name, fallback, minimum, maximum) => {
|
|
5866
|
+
const raw = option(name);
|
|
5867
|
+
if (raw === void 0) return { ok: true, value: fallback };
|
|
5868
|
+
const parsed = Number(raw);
|
|
5869
|
+
return Number.isSafeInteger(parsed) && parsed >= minimum && parsed <= maximum ? { ok: true, value: parsed } : {
|
|
5870
|
+
ok: false,
|
|
5871
|
+
error: {
|
|
5872
|
+
code: "cli-parse-error",
|
|
5873
|
+
expected: `${name} to be an integer from ${minimum} to ${maximum}`,
|
|
5874
|
+
hint: "Pass a bounded deterministic capture value.",
|
|
5875
|
+
detail: { option: name, received: raw }
|
|
5876
|
+
}
|
|
5877
|
+
};
|
|
5878
|
+
};
|
|
5879
|
+
const width = parseInteger("--width", 1280, 64, 8192);
|
|
5880
|
+
if (!width.ok) return width;
|
|
5881
|
+
const height = parseInteger("--height", 720, 64, 8192);
|
|
5882
|
+
if (!height.ok) return height;
|
|
5883
|
+
const waitMs = parseInteger("--wait-ms", 4e3, 0, 12e4);
|
|
5884
|
+
if (!waitMs.ok) return waitMs;
|
|
5885
|
+
const output = option("--output");
|
|
5886
|
+
const browser = option("--browser");
|
|
5887
|
+
return browserCaptureCommand({
|
|
5888
|
+
root: projectRoot,
|
|
5889
|
+
json,
|
|
5890
|
+
backend: requestedBackend ?? (software ? "software" : "auto"),
|
|
5891
|
+
...software ? { software: true } : {},
|
|
5892
|
+
width: width.value,
|
|
5893
|
+
height: height.value,
|
|
5894
|
+
waitMs: waitMs.value,
|
|
5895
|
+
requireUi: args.includes("--require-ui"),
|
|
5896
|
+
deterministic: args.includes("--deterministic"),
|
|
5897
|
+
...args.includes("--headless") ? { headless: true } : {},
|
|
5898
|
+
...parsedPort.value === void 0 ? {} : { port: parsedPort.value },
|
|
5899
|
+
...output === void 0 ? {} : { output },
|
|
5900
|
+
...browser === void 0 ? {} : { browser }
|
|
5901
|
+
});
|
|
5902
|
+
}
|
|
5903
|
+
case "engine.status":
|
|
5904
|
+
return engineStatusCommand({ root: scopedRoot});
|
|
5905
|
+
case "engine.use-local": {
|
|
5906
|
+
const path = positionals[0];
|
|
5907
|
+
if (path === void 0) {
|
|
5908
|
+
return {
|
|
5909
|
+
ok: false,
|
|
5910
|
+
error: {
|
|
5911
|
+
code: "cli-parse-error",
|
|
5912
|
+
expected: "forgeax engine use-local <engine-directory>",
|
|
5913
|
+
hint: "Pass the local Engine source checkout or SDK source directory.",
|
|
5914
|
+
detail: {}
|
|
5915
|
+
}
|
|
5916
|
+
};
|
|
5917
|
+
}
|
|
5918
|
+
return engineUseLocalCommand({ root: scopedRoot, path, dryRun, json });
|
|
5919
|
+
}
|
|
5920
|
+
case "engine.unlink":
|
|
5921
|
+
return engineUnlinkCommand({ root: scopedRoot, dryRun});
|
|
5922
|
+
case "engine.doctor":
|
|
5923
|
+
return engineDoctorCommand({ root: scopedRoot});
|
|
4715
5924
|
case "preview": {
|
|
4716
5925
|
const options = projectServerOptions();
|
|
4717
5926
|
return options.ok ? previewCommand(options.value) : options;
|
|
@@ -4892,7 +6101,29 @@ async function run(value) {
|
|
|
4892
6101
|
}
|
|
4893
6102
|
};
|
|
4894
6103
|
}
|
|
4895
|
-
|
|
6104
|
+
const fieldsOption = option("--fields");
|
|
6105
|
+
const fields = fieldsOption?.split(",").map((field) => field.trim()).filter((field) => field.length > 0);
|
|
6106
|
+
const allowedFields = /* @__PURE__ */ new Set(["bindings", "pipeline", "pixels"]);
|
|
6107
|
+
if (fields?.some((field) => !allowedFields.has(field))) {
|
|
6108
|
+
return {
|
|
6109
|
+
ok: false,
|
|
6110
|
+
error: {
|
|
6111
|
+
code: "cli-parse-error",
|
|
6112
|
+
expected: "--fields to contain pipeline, bindings, and/or pixels",
|
|
6113
|
+
hint: "Use a comma-separated subset such as --fields pipeline,bindings.",
|
|
6114
|
+
detail: { fields: fieldsOption }
|
|
6115
|
+
}
|
|
6116
|
+
};
|
|
6117
|
+
}
|
|
6118
|
+
return runRhiDebugCommand(
|
|
6119
|
+
operation,
|
|
6120
|
+
{
|
|
6121
|
+
artifact,
|
|
6122
|
+
workIndex,
|
|
6123
|
+
...fields === void 0 ? {} : { fields }
|
|
6124
|
+
},
|
|
6125
|
+
context
|
|
6126
|
+
);
|
|
4896
6127
|
}
|
|
4897
6128
|
}
|
|
4898
6129
|
}
|