@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.
Files changed (36) hide show
  1. package/README.md +143 -1
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/__tests__/engine-binding.unit.test.d.ts +2 -0
  4. package/dist/__tests__/engine-binding.unit.test.d.ts.map +1 -0
  5. package/dist/__tests__/software-browser-session.unit.test.d.ts +2 -0
  6. package/dist/__tests__/software-browser-session.unit.test.d.ts.map +1 -0
  7. package/dist/__tests__/software-capture-pixels.unit.test.d.ts +2 -0
  8. package/dist/__tests__/software-capture-pixels.unit.test.d.ts.map +1 -0
  9. package/dist/__tests__/software-capture.unit.test.d.ts +2 -0
  10. package/dist/__tests__/software-capture.unit.test.d.ts.map +1 -0
  11. package/dist/cli-output.d.ts.map +1 -1
  12. package/dist/cli.mjs +1477 -246
  13. package/dist/cli.mjs.map +1 -1
  14. package/dist/commands.d.ts +2 -0
  15. package/dist/commands.d.ts.map +1 -1
  16. package/dist/engine-binding.d.ts +58 -0
  17. package/dist/engine-binding.d.ts.map +1 -0
  18. package/dist/host.d.ts.map +1 -1
  19. package/dist/index.d.ts +7 -2
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.mjs +1414 -295
  22. package/dist/index.mjs.map +1 -1
  23. package/dist/rhi-debug/cli-context.d.ts.map +1 -1
  24. package/dist/rhi-debug/operations.d.ts +3 -3
  25. package/dist/rhi-debug/operations.d.ts.map +1 -1
  26. package/dist/sdk-bootstrap.d.ts.map +1 -1
  27. package/dist/sdk-cli.mjs +4 -2
  28. package/dist/sdk-cli.mjs.map +1 -1
  29. package/dist/software-capture.d.ts +123 -0
  30. package/dist/software-capture.d.ts.map +1 -0
  31. package/dist/tools/browser-host.d.ts.map +1 -1
  32. package/dist/tools/commands.d.ts +5 -0
  33. package/dist/tools/commands.d.ts.map +1 -1
  34. package/dist/types.d.ts +21 -1
  35. package/dist/types.d.ts.map +1 -1
  36. 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, realpath, readlink, lstat, rmdir, access } from 'fs/promises';
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-null';
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 (engineWorkspacePackagesPromise !== void 0) return engineWorkspacePackagesPromise;
375
- engineWorkspacePackagesPromise = (async () => {
376
- const workspaceRoot = findEngineWorkspaceRoot();
377
- if (workspaceRoot === void 0) return /* @__PURE__ */ new Map();
378
- const packageRoot = resolve(workspaceRoot, "packages");
379
- if (!existsSync(packageRoot)) return /* @__PURE__ */ new Map();
380
- const packages = /* @__PURE__ */ new Map();
381
- for (const entry of await readdir(packageRoot, { withFileTypes: true })) {
382
- if (!entry.isDirectory()) continue;
383
- const root = resolve(packageRoot, entry.name);
384
- try {
385
- const manifest = JSON.parse(
386
- await readFile(resolve(root, "package.json"), "utf8")
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
- return packages;
397
- })();
398
- return engineWorkspacePackagesPromise;
921
+ }
922
+ return packages;
399
923
  }
400
- function conditionalExport(value) {
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 = conditionalExport(candidate);
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 ? conditionalExport(exportsValue) : void 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 ? conditionalExport(exportsValue) : void 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 conditionalExport(exact);
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 = conditionalExport(exportTarget);
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-")) return void 0;
443
- const separator = source.indexOf("/", "@forgeax/engine-".length);
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 packages = await engineWorkspacePackages();
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-")) return null;
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
- assets.configureRuntimeBinding(runtimeScopeBinding);
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
- for (const key of ['code', 'expected', 'hint']) {
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
- const message = formatStartupFailure(reason);
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
- code: fallbackCode,
1406
- expected: "the ForgeaX command to complete",
1407
- hint: "Inspect the underlying diagnostic and repair the owning input.",
1408
- detail: { reason: cause instanceof Error ? cause.message : String(cause) }
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 init_project = __esm({
1412
- "src/project.ts"() {
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.1.5"
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.1.5"
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 readManifest(root) {
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 readManifest(root);
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 readManifest(root);
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 rhi-null adapter",
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 rhi-null device",
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: { type: "array", items: { type: "string" } }
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-default]\n forgeax <init|doctor|test|dev|build|package|serve|preview> [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 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";
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 value = await program(client);
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
- return runRhiDebugCommand(operation, { artifact, workIndex }, context);
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
  }