@forgeax/engine-devkit 0.1.4 → 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 (68) hide show
  1. package/README.md +162 -5
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/__tests__/cli-output.unit.test.d.ts +2 -0
  4. package/dist/__tests__/cli-output.unit.test.d.ts.map +1 -0
  5. package/dist/__tests__/engine-binding.unit.test.d.ts +2 -0
  6. package/dist/__tests__/engine-binding.unit.test.d.ts.map +1 -0
  7. package/dist/__tests__/gltf-source-key-diagnostics.integration.test.d.ts +2 -0
  8. package/dist/__tests__/gltf-source-key-diagnostics.integration.test.d.ts.map +1 -0
  9. package/dist/__tests__/preview-port.integration.test.d.ts +2 -0
  10. package/dist/__tests__/preview-port.integration.test.d.ts.map +1 -0
  11. package/dist/__tests__/rhi-debug-build-graph.integration.test.d.ts +2 -0
  12. package/dist/__tests__/rhi-debug-build-graph.integration.test.d.ts.map +1 -0
  13. package/dist/__tests__/sdk-update.unit.test.d.ts +2 -0
  14. package/dist/__tests__/sdk-update.unit.test.d.ts.map +1 -0
  15. package/dist/__tests__/software-browser-session.unit.test.d.ts +2 -0
  16. package/dist/__tests__/software-browser-session.unit.test.d.ts.map +1 -0
  17. package/dist/__tests__/software-capture-pixels.unit.test.d.ts +2 -0
  18. package/dist/__tests__/software-capture-pixels.unit.test.d.ts.map +1 -0
  19. package/dist/__tests__/software-capture.unit.test.d.ts +2 -0
  20. package/dist/__tests__/software-capture.unit.test.d.ts.map +1 -0
  21. package/dist/assets.d.ts.map +1 -1
  22. package/dist/bootstrap-commands.d.ts.map +1 -1
  23. package/dist/cli-output.d.ts +4 -0
  24. package/dist/cli-output.d.ts.map +1 -0
  25. package/dist/cli.mjs +1863 -819
  26. package/dist/cli.mjs.map +1 -1
  27. package/dist/commands.d.ts +2 -0
  28. package/dist/commands.d.ts.map +1 -1
  29. package/dist/engine-binding.d.ts +58 -0
  30. package/dist/engine-binding.d.ts.map +1 -0
  31. package/dist/host/__tests__/bootstrap-root-owner.test-d.d.ts +2 -0
  32. package/dist/host/__tests__/bootstrap-root-owner.test-d.d.ts.map +1 -0
  33. package/dist/host/project-bootstrap.d.ts +4 -2
  34. package/dist/host/project-bootstrap.d.ts.map +1 -1
  35. package/dist/host/resource-bootstrap.d.ts +5 -3
  36. package/dist/host/resource-bootstrap.d.ts.map +1 -1
  37. package/dist/host.d.ts +2 -1
  38. package/dist/host.d.ts.map +1 -1
  39. package/dist/index.d.ts +11 -2
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.mjs +1714 -862
  42. package/dist/index.mjs.map +1 -1
  43. package/dist/project.d.ts +0 -1
  44. package/dist/project.d.ts.map +1 -1
  45. package/dist/rhi-debug/cli-context.d.ts.map +1 -1
  46. package/dist/rhi-debug/operations.d.ts +3 -3
  47. package/dist/rhi-debug/operations.d.ts.map +1 -1
  48. package/dist/sdk-bootstrap.d.ts +9 -0
  49. package/dist/sdk-bootstrap.d.ts.map +1 -1
  50. package/dist/sdk-cli.mjs +193 -25
  51. package/dist/sdk-cli.mjs.map +1 -1
  52. package/dist/sdk-install.d.ts.map +1 -1
  53. package/dist/sdk-update.d.ts +23 -0
  54. package/dist/sdk-update.d.ts.map +1 -0
  55. package/dist/software-capture.d.ts +123 -0
  56. package/dist/software-capture.d.ts.map +1 -0
  57. package/dist/tools/benchmark/__tests__/benchmark-vocabulary-owner.test-d.d.ts +2 -0
  58. package/dist/tools/benchmark/__tests__/benchmark-vocabulary-owner.test-d.d.ts.map +1 -0
  59. package/dist/tools/benchmark/harness.d.ts.map +1 -1
  60. package/dist/tools/browser-host.d.ts.map +1 -1
  61. package/dist/tools/commands.d.ts +5 -0
  62. package/dist/tools/commands.d.ts.map +1 -1
  63. package/dist/tools/native-preview.d.ts.map +1 -1
  64. package/dist/types.d.ts +28 -1
  65. package/dist/types.d.ts.map +1 -1
  66. package/package.json +32 -31
  67. package/dist/__tests__/tool-migration.test.d.ts +0 -2
  68. package/dist/__tests__/tool-migration.test.d.ts.map +0 -1
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,15 +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 { build, createServer, preview } from 'vite';
29
+ import { decodeTape, openReplay, buildFrameModel, replayDeviceRequest, createRhiDebugError } from '@forgeax/engine-rhi-debug';
30
+ import { rhi, createShaderModule } from '@forgeax/engine-rhi-webgpu';
31
+ import { build, preview, createServer as createServer$1 } from 'vite';
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';
31
36
  import { startVitest } from 'vitest/node';
32
37
  import materialPreviewPlugin from '@forgeax/engine-preview/material';
33
38
  import meshPreviewPlugin from '@forgeax/engine-preview/mesh';
@@ -299,17 +304,546 @@ var init_dist = __esm({
299
304
  "src/dist.ts"() {
300
305
  }
301
306
  });
302
- function ignoreDevKitCatalogPath(path) {
303
- const normalized = path.replace(/\\/g, "/");
304
- if (normalized.split("/").includes("shaders")) return true;
305
- if (!normalized.endsWith(".meta.json")) return false;
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);
306
394
  try {
307
- const meta = JSON.parse(readFileSync(path, "utf8"));
308
- return meta.importer === "game-default-target-profile";
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;
309
459
  } catch {
310
460
  return false;
311
461
  }
312
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
+ });
835
+
836
+ // src/host.ts
837
+ var host_exports = {};
838
+ __export(host_exports, {
839
+ createViteConfig: () => createViteConfig,
840
+ devKitDdcRoots: () => devKitDdcRoots,
841
+ ignoreDevKitCatalogPath: () => ignoreDevKitCatalogPath
842
+ });
843
+ function ignoreDevKitCatalogPath(path) {
844
+ const normalized = path.replace(/\\/g, "/");
845
+ return normalized.split("/").includes("shaders");
846
+ }
313
847
  function devKitDdcRoots(projectRoot2) {
314
848
  return {
315
849
  buildCacheRoot: resolve(projectRoot2, ".forgeax", "ddc", "build-cache"),
@@ -367,40 +901,33 @@ function findEngineWorkspaceRoot() {
367
901
  cursor = parent;
368
902
  }
369
903
  }
370
- async function engineWorkspacePackages() {
371
- if (engineWorkspacePackagesPromise !== void 0) return engineWorkspacePackagesPromise;
372
- engineWorkspacePackagesPromise = (async () => {
373
- const workspaceRoot = findEngineWorkspaceRoot();
374
- if (workspaceRoot === void 0) return /* @__PURE__ */ new Map();
375
- const packageRoot = resolve(workspaceRoot, "packages");
376
- if (!existsSync(packageRoot)) return /* @__PURE__ */ new Map();
377
- const packages = /* @__PURE__ */ new Map();
378
- for (const entry of await readdir(packageRoot, { withFileTypes: true })) {
379
- if (!entry.isDirectory()) continue;
380
- const root = resolve(packageRoot, entry.name);
381
- try {
382
- const manifest = JSON.parse(
383
- await readFile(resolve(root, "package.json"), "utf8")
384
- );
385
- if (manifest === null || typeof manifest !== "object") continue;
386
- const name = manifest.name;
387
- if (typeof name === "string" && name.startsWith("@forgeax/engine-")) {
388
- packages.set(name, { root, manifest });
389
- }
390
- } 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 });
391
918
  }
919
+ } catch {
392
920
  }
393
- return packages;
394
- })();
395
- return engineWorkspacePackagesPromise;
921
+ }
922
+ return packages;
396
923
  }
397
- function conditionalExport(value) {
924
+ function conditionalExport2(value) {
398
925
  if (typeof value === "string") return value;
399
926
  if (value === null || Array.isArray(value)) return void 0;
400
927
  for (const condition of ["browser", "import", "node", "default"]) {
401
928
  const candidate = value[condition];
402
929
  if (candidate === void 0) continue;
403
- const selected = conditionalExport(candidate);
930
+ const selected = conditionalExport2(candidate);
404
931
  if (selected !== void 0) return selected;
405
932
  }
406
933
  return void 0;
@@ -413,14 +940,14 @@ function packageExportTarget(manifest, subpath) {
413
940
  return typeof main === "string" ? main : void 0;
414
941
  }
415
942
  if (typeof exportsValue === "string" || exportsValue === null) {
416
- return subpath.length === 0 ? conditionalExport(exportsValue) : void 0;
943
+ return subpath.length === 0 ? conditionalExport2(exportsValue) : void 0;
417
944
  }
418
945
  const keys = Object.keys(exportsValue);
419
946
  const subpathMap = keys.some((key) => key === "." || key.startsWith("./"));
420
- if (!subpathMap) return subpath.length === 0 ? conditionalExport(exportsValue) : void 0;
947
+ if (!subpathMap) return subpath.length === 0 ? conditionalExport2(exportsValue) : void 0;
421
948
  const requested = subpath.length === 0 ? "." : `./${subpath}`;
422
949
  const exact = exportsValue[requested];
423
- if (exact !== void 0) return conditionalExport(exact);
950
+ if (exact !== void 0) return conditionalExport2(exact);
424
951
  for (const key of keys) {
425
952
  const marker = key.indexOf("*");
426
953
  if (marker < 0) continue;
@@ -430,14 +957,14 @@ function packageExportTarget(manifest, subpath) {
430
957
  const replacement = requested.slice(prefix.length, requested.length - suffix.length);
431
958
  const exportTarget = exportsValue[key];
432
959
  if (exportTarget === void 0) continue;
433
- const selected = conditionalExport(exportTarget);
960
+ const selected = conditionalExport2(exportTarget);
434
961
  return selected?.replaceAll("*", replacement);
435
962
  }
436
963
  return void 0;
437
964
  }
438
965
  function engineWorkspaceImport(source, packages) {
439
- if (!source.startsWith("@forgeax/engine-")) return void 0;
440
- const separator = source.indexOf("/", "@forgeax/engine-".length);
966
+ if (!source.startsWith("@forgeax/engine")) return void 0;
967
+ const separator = source.indexOf("/", "@forgeax/engine".length);
441
968
  const packageName = separator < 0 ? source : source.slice(0, separator);
442
969
  const subpath = separator < 0 ? "" : source.slice(separator + 1);
443
970
  const packageInfo = packages.get(packageName);
@@ -451,14 +978,25 @@ function engineWorkspaceImport(source, packages) {
451
978
  }
452
979
  return absolute;
453
980
  }
454
- async function createEngineWorkspaceResolver() {
455
- 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);
456
992
  if (packages.size === 0) return void 0;
457
993
  return {
458
994
  name: "forgeax:devkit-engine-workspace-resolver",
995
+ enforce: "pre",
459
996
  async resolveId(source, importer) {
460
997
  const bareSource = source.split("?", 1)[0] ?? source;
461
- if (!bareSource.startsWith("@forgeax/engine-")) return null;
998
+ if (!bareSource.startsWith("@forgeax/engine")) return null;
999
+ if (localRoot !== void 0) return engineWorkspaceImport(bareSource, packages);
462
1000
  try {
463
1001
  const resolved = await this.resolve(source, importer, { skipSelf: true });
464
1002
  if (resolved !== null) return resolved;
@@ -574,17 +1112,20 @@ await app.pluginContext.plugin({
574
1112
  return `import { forgeaxBundlerAdapter } from 'virtual:forgeax/bundler';
575
1113
  import { createApp, createToolPreviewHost, createToolPreviewRecipe, fitToolPreviewCameraToAabb, gameHostPlugin, replayToolPreviewCapture } from '@forgeax/engine/app';
576
1114
  import { createCatalogSource } from '@forgeax/engine/assets-runtime';
1115
+ import {
1116
+ createRuntimeAssetImportTransport,
1117
+ runtimeBinding,
1118
+ } from 'virtual:forgeax/pack-runtime';
577
1119
  import { installCatalogLoader, projectPluginEntries } from '@forgeax/engine/plugin/loader';
578
1120
  import { audioPlugin } from '@forgeax/engine/audio';
579
1121
  import { webAudioPlugin } from '@forgeax/engine/audio-webaudio';
580
1122
  import { physicsPlugin } from '@forgeax/engine/physics';
581
1123
  import { skinningPlugin } from '@forgeax/engine/skinning';
582
1124
  import { createPrimitiveMesh } from '@forgeax/engine/geometry';
583
- import { createDevImportTransport } from '@forgeax/engine/runtime';
584
1125
  import { mat4 } from '@forgeax/engine/math';
585
1126
  import { CAMERA_PROJECTION_ORTHOGRAPHIC, Camera, DirectionalLight, Materials, MeshFilter, MeshRenderer, Skylight, SkyboxBackground, TONEMAP_NONE, TONEMAP_REINHARD_EXTENDED } from '@forgeax/engine/render';
586
- import { projectSceneAsset, Transform, worldInstantiateScene } from '@forgeax/engine/scene';
587
- import { createStandaloneRuntimeAssetBinding } from '@forgeax/engine/types';
1127
+ import { Transform } from '@forgeax/engine/scene';
1128
+ import { type SceneAsset } from '@forgeax/engine/types';
588
1129
  import { ParticleEffectPlayer, vfxGpuEffectContribution } from '@forgeax/engine/vfx';
589
1130
  import { createVfxRuntimeHost } from '@forgeax/engine/vfx-render';
590
1131
 
@@ -685,17 +1226,43 @@ const resizeCanvas = () => {
685
1226
  const resizeObserver = new ResizeObserver(resizeCanvas);
686
1227
  resizeObserver.observe(canvas);
687
1228
  resizeCanvas();
688
- const runtimeScopeBinding = createStandaloneRuntimeAssetBinding(${JSON.stringify(facts.id)});
1229
+ const runtimeScopeBinding = runtimeBinding;
1230
+ if (runtimeScopeBinding === undefined) {
1231
+ throw new Error('forgeax: Vite Pack runtime binding is required in the generated host');
1232
+ }
689
1233
  const assetCatalog = createCatalogSource({
690
1234
  url: import.meta.env.DEV
691
1235
  ? runtimeScopeBinding.catalogUrl
692
1236
  : new URL('pack-index.json', document.baseURI).href,
693
- expectedScope: runtimeScopeBinding,
1237
+ ...(import.meta.env.DEV ? { expectedScope: runtimeScopeBinding } : {}),
694
1238
  });
695
1239
  const bundler = {
696
1240
  ...forgeaxBundlerAdapter(),
697
- ...(import.meta.env.DEV ? { importTransport: createDevImportTransport(runtimeScopeBinding) } : {}),
1241
+ ...(import.meta.env.DEV
1242
+ ? { importTransport: createRuntimeAssetImportTransport(runtimeScopeBinding) }
1243
+ : {}),
698
1244
  };
1245
+ const assetPreparation = new WeakMap();
1246
+
1247
+ function prepareAssetRegistry(assets) {
1248
+ const existing = assetPreparation.get(assets);
1249
+ if (existing !== undefined) return existing;
1250
+ const pending = (async () => {
1251
+ if (import.meta.env.DEV) {
1252
+ assets.configureRuntimeBinding(runtimeScopeBinding);
1253
+ } else {
1254
+ assets.configurePackIndex(new URL('pack-index.json', document.baseURI).href);
1255
+ }
1256
+ assets.setCatalogSource(assetCatalog);
1257
+ if (vfxRuntimeHost !== undefined) {
1258
+ assets.installDecoder(vfxGpuEffectContribution.kind, vfxGpuEffectContribution.decoder);
1259
+ }
1260
+ const catalog = await assets.enumerateCatalog();
1261
+ if (!catalog.ok) throw catalog.error;
1262
+ })();
1263
+ assetPreparation.set(assets, pending);
1264
+ return pending;
1265
+ }
699
1266
 
700
1267
  function previewStable(value) {
701
1268
  if (value instanceof ArrayBuffer) return 'ArrayBuffer:' + JSON.stringify(Array.from(new Uint8Array(value)));
@@ -756,13 +1323,14 @@ async function previewOwnerFacts(assets, resource, payload) {
756
1323
  }
757
1324
 
758
1325
  async function prepareProject(app) {
1326
+ await prepareAssetRegistry(app.assets);
759
1327
  previewWorld = app.world;
760
1328
  const assets = app.assets;
761
1329
  let canonicalEnvironment;
762
1330
  if (resource !== undefined) {
763
1331
  if (resource.kind !== 'texture') {
764
1332
  if (canonicalEnvironmentGuid === null) throw new Error('canonical preview environment is unavailable');
765
- const environment = await assets.load(canonicalEnvironmentGuid, 'equirect');
1333
+ const environment = await assets.loadByGuid(assets.parseGuid(canonicalEnvironmentGuid));
766
1334
  if (!environment.ok) throw environment.error;
767
1335
  if (environment.value.kind !== 'equirect') {
768
1336
  throw new Error(\`canonical preview environment expected equirect, received \${environment.value.kind}\`);
@@ -777,7 +1345,7 @@ if (resource !== undefined) {
777
1345
  const attached = await vfxRuntimeHost.attachWorld({ world: app.world, assets });
778
1346
  if (!attached.ok) throw attached.error;
779
1347
  }
780
- const loaded = await assets.load(resource.guid, resource.kind === 'vfx' ? 'particle-effect' : resource.kind);
1348
+ const loaded = await assets.loadByGuid(assets.parseGuid(resource.guid));
781
1349
  if (!loaded.ok) throw loaded.error;
782
1350
  const payload = loaded.value;
783
1351
  if (resource.kind === 'vfx' && payload.kind !== 'particle-effect') {
@@ -790,7 +1358,7 @@ if (resource !== undefined) {
790
1358
  const meshMaterialHandles = resource.kind === 'mesh' && payload.kind === 'mesh'
791
1359
  ? await Promise.all(payload.materialSlots.map(async (slot) => {
792
1360
  if (slot.defaultMaterial === undefined) return 0;
793
- const material = await assets.load(slot.defaultMaterial, 'material');
1361
+ const material = await assets.loadByGuid(assets.parseGuid(slot.defaultMaterial));
794
1362
  if (!material.ok) throw material.error;
795
1363
  if (material.value.kind !== 'material') {
796
1364
  throw new Error('mesh material slot resolved to a non-material asset');
@@ -920,15 +1488,13 @@ let defaultScene;
920
1488
  let defaultSceneRoot;
921
1489
  const defaultSceneGuid = ${JSON.stringify(facts.defaultScene)};
922
1490
  if (defaultSceneGuid !== undefined) {
923
- const loaded = await assets.load(defaultSceneGuid, 'scene');
1491
+ const loaded = await assets.loadByGuid<SceneAsset>(assets.parseGuid(defaultSceneGuid));
924
1492
  if (!loaded.ok) throw loaded.error;
925
- const projected = await projectSceneAsset(app.world, loaded.value, (guid, kind) => assets.load(guid, kind));
926
- if (!projected.ok) throw projected.error;
927
- defaultScene = projected.value;
928
- const handle = app.world.allocSharedRef('SceneAsset', projected.value);
929
- const instantiated = worldInstantiateScene(app.world, handle);
1493
+ defaultScene = loaded.value;
1494
+ const handle = app.world.allocSharedRef('SceneAsset', loaded.value);
1495
+ const instantiated = assets.instantiate<SceneAsset>(handle, app.world);
930
1496
  if (!instantiated.ok) throw instantiated.error;
931
- defaultSceneRoot = instantiated.value.root;
1497
+ defaultSceneRoot = instantiated.value;
932
1498
  }
933
1499
  const uiRoot = document.querySelector('#game-ui');
934
1500
  await app.pluginContext.plugin(gameHostPlugin({
@@ -967,8 +1533,6 @@ if (query.has('forgeax-tool-replay')) {
967
1533
  ...(resource === undefined ? {} : { resource }),
968
1534
  canvas,
969
1535
  app: {
970
- ...(assetCatalog === undefined ? {} : { assetCatalog }),
971
- ...(vfxRuntimeHost === undefined ? {} : { assetDecoders: [vfxGpuEffectContribution] }),
972
1536
  plugins: [${plugins.join(", ")}],
973
1537
  ...(vfxRuntimeHost === undefined ? {} : { features: [vfxRuntimeHost.feature] }),
974
1538
  },
@@ -1081,8 +1645,6 @@ if (query.has('forgeax-tool-replay')) {
1081
1645
  const result = await createApp(
1082
1646
  canvas,
1083
1647
  {
1084
- ...(assetCatalog === undefined ? {} : { assetCatalog }),
1085
- ...(vfxRuntimeHost === undefined ? {} : { assetDecoders: [vfxGpuEffectContribution] }),
1086
1648
  plugins: [${plugins.join(", ")}],
1087
1649
  ...(pointerLockAllowed === undefined ? {} : { pointerLockAllowed }),
1088
1650
  },
@@ -1143,32 +1705,62 @@ function htmlSource(title) {
1143
1705
  <div id="app-shell"><canvas id="app"></canvas><div id="game-ui"></div></div><div id="forgeax-fatal" role="alert"></div>
1144
1706
  <script>
1145
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
+ };
1146
1744
  const formatStartupFailure = (reason) => {
1147
- if (reason instanceof Error) return reason.message;
1148
1745
  if (reason !== null && typeof reason === 'object') {
1149
- const record = reason;
1150
1746
  const lines = [];
1151
- for (const key of ['code', 'expected', 'hint']) {
1152
- if (typeof record[key] === 'string' && record[key].length > 0) {
1153
- lines.push(key + ': ' + record[key]);
1154
- }
1155
- }
1156
- const detail = record.detail;
1157
- if (detail !== null && typeof detail === 'object') {
1158
- for (const key of ['reason', 'guid']) {
1159
- if (typeof detail[key] === 'string' && detail[key].length > 0) {
1160
- lines.push('detail.' + key + ': ' + detail[key]);
1161
- }
1162
- }
1163
- }
1747
+ appendStructuredFailure(reason, '', 0, new Set(), lines);
1164
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
+ }
1165
1754
  }
1166
1755
  return String(reason ?? 'Unknown startup failure');
1167
1756
  };
1168
1757
  const show = (reason) => {
1169
1758
  const notice = document.querySelector('#forgeax-fatal');
1170
1759
  if (!(notice instanceof HTMLElement)) return;
1171
- 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
+ }
1172
1764
  notice.textContent = 'ForgeaX game failed to start.\\n' + message;
1173
1765
  notice.style.display = 'grid';
1174
1766
  };
@@ -1194,215 +1786,88 @@ async function createViteConfig(facts, command2, base = "/", options = {}) {
1194
1786
  writeFile(resolve(generated, "main.ts"), hostSource(facts, bootstrapRoot, canonicalKit?.guid))
1195
1787
  ]);
1196
1788
  const roots = [
1197
- ...projectRoots,
1198
- ...builtinPack === void 0 ? [] : [builtinPack],
1199
- ...canonicalKit === void 0 ? [] : [canonicalKit.root]
1200
- ];
1201
- const importers = [...projectImporters(facts)];
1202
- const runtimeBinding = createStandaloneRuntimeAssetBinding(facts.id);
1203
- const engineWorkspaceResolver = await createEngineWorkspaceResolver();
1204
- const consumerAliases = await consumerEngineAliases(facts.root);
1205
- const plugins = [
1206
- ...engineWorkspaceResolver === void 0 ? [] : [engineWorkspaceResolver],
1207
- forgeaxShader(),
1208
- pluginPack({
1209
- roots,
1210
- runtimeBinding,
1211
- ddc: devKitDdcRoots(facts.root),
1212
- refresh: command2 === "serve" ? reloadAssetHost() : void 0,
1213
- importers: [
1214
- audioImporter,
1215
- imageImporter,
1216
- fbxImporter,
1217
- gltfImporter,
1218
- fontImporter,
1219
- ...importers
1220
- ],
1221
- cookers: [createMaterialPackCooker(roots), createParticleCodeNativeCookerFromRoots(roots)],
1222
- ignorePath
1223
- })
1224
- ];
1225
- return {
1226
- root: generated,
1227
- base,
1228
- configFile: false,
1229
- publicDir: facts.assetPublicDir === void 0 ? false : resolve(facts.root, facts.assetPublicDir),
1230
- plugins,
1231
- resolve: {
1232
- alias: consumerAliases,
1233
- dedupe: ["@forgeax/engine-app", "@forgeax/engine-ecs", "@forgeax/engine-runtime"]
1234
- },
1235
- server: { fs: { allow: [facts.root, ...roots] } },
1236
- build: {
1237
- target: "esnext",
1238
- outDir: options.outDir ?? resolve(facts.root, "dist"),
1239
- emptyOutDir: true,
1240
- rollupOptions: { input: resolve(generated, "index.html") }
1241
- }
1242
- };
1243
- }
1244
- var hostRequire, engineWorkspacePackagesPromise;
1245
- var init_host = __esm({
1246
- "src/host.ts"() {
1247
- hostRequire = createRequire(import.meta.url);
1248
- }
1249
- });
1250
- function projectError(code, expected, hint, detail) {
1251
- return { ok: false, error: { code, expected, hint, detail } };
1252
- }
1253
- async function readJson(path) {
1254
- return JSON.parse(await readFile(path, "utf8"));
1255
- }
1256
- function firstUnsupportedStandaloneRealm(entries2, inheritedRealm = "engine") {
1257
- for (const entry of entries2) {
1258
- const realm = entry.realm ?? inheritedRealm ?? "engine";
1259
- if (realm === "host") return { id: entry.id, realm };
1260
- if (entry.group === true) {
1261
- const unsupported = firstUnsupportedStandaloneRealm(
1262
- entry.config,
1263
- realm
1264
- );
1265
- if (unsupported !== void 0) return unsupported;
1266
- }
1267
- }
1268
- return void 0;
1269
- }
1270
- function pluginModuleNames(entries2) {
1271
- return entries2.flatMap(
1272
- (entry) => entry.group === true ? pluginModuleNames(entry.config) : [entry.name]
1273
- );
1274
- }
1275
- async function readProjectFacts(rootInput = process.cwd()) {
1276
- const root = resolve(rootInput);
1277
- let forgeValue;
1278
- let packageValue;
1279
- try {
1280
- [forgeValue, packageValue] = await Promise.all([
1281
- readJson(resolve(root, "forge.json")),
1282
- readJson(resolve(root, "package.json"))
1283
- ]);
1284
- } catch (cause) {
1285
- return projectError(
1286
- "project-manifest-unreadable",
1287
- "readable forge.json and package.json files",
1288
- "Run the command from a ForgeaX game root or pass its directory.",
1289
- { root, reason: cause instanceof Error ? cause.message : String(cause) }
1290
- );
1291
- }
1292
- if (forgeValue === null || typeof forgeValue !== "object") {
1293
- return projectError(
1294
- "project-manifest-invalid",
1295
- "forge.json to contain an object",
1296
- "Repair forge.json before running DevKit.",
1297
- { root }
1298
- );
1299
- }
1300
- if (packageValue === null || typeof packageValue !== "object") {
1301
- return projectError(
1302
- "package-manifest-invalid",
1303
- "package.json to contain an object",
1304
- "Repair package.json before running DevKit.",
1305
- { root }
1306
- );
1307
- }
1308
- const parsedForge = GameProjectSchema.safeParse(forgeValue);
1309
- if (!parsedForge.success) {
1310
- return projectError(
1311
- "project-manifest-invalid",
1312
- "forge.json to satisfy @forgeax/engine-project GameProjectSchema",
1313
- "Repair the fields reported by the authoritative project schema.",
1314
- { root, issues: parsedForge.error.issues }
1315
- );
1316
- }
1317
- const forge = parsedForge.data;
1318
- if (forge.id.length === 0 || forge.name.length === 0 || forge.entry === void 0 || forge.entry.length === 0) {
1319
- return projectError(
1320
- "project-manifest-invalid",
1321
- "forge.json to declare id, name, and entry",
1322
- "Restore the project entry; plugin Entries remain optional additions.",
1323
- { root }
1324
- );
1325
- }
1326
- const plugins = forge.plugins ?? [];
1327
- const unsupportedRealm = firstUnsupportedStandaloneRealm(plugins);
1328
- if (unsupportedRealm !== void 0) {
1329
- return projectError(
1330
- "project-plugin-realm-unsupported",
1331
- "the standalone Devkit host to contain only engine-realm plugin Entries",
1332
- "Move Host or build plugins to a host that owns that physical realm.",
1333
- { root, ...unsupportedRealm }
1334
- );
1335
- }
1336
- const entryPath = isAbsolute(forge.entry) ? forge.entry : resolve(root, forge.entry);
1337
- try {
1338
- await readFile(entryPath);
1339
- } catch {
1340
- return projectError(
1341
- "project-entry-missing",
1342
- "forge.json#entry to resolve to a readable module",
1343
- "Restore the game entry or update forge.json#entry.",
1344
- { root, entry: forge.entry }
1345
- );
1346
- }
1347
- const packageJson = packageValue;
1348
- const forgeax = packageJson.forgeax;
1349
- const configuredRoots = forgeax !== null && typeof forgeax === "object" ? forgeax.assets?.roots : void 0;
1350
- const assetRoots = Array.isArray(configuredRoots) && configuredRoots.every((value) => typeof value === "string") ? configuredRoots : ["assets"];
1351
- const configuredImporters = forgeax !== null && typeof forgeax === "object" ? forgeax.assets?.importers : void 0;
1352
- const assetImporters = Array.isArray(configuredImporters) && configuredImporters.every((value) => typeof value === "string") ? configuredImporters : [];
1353
- const configuredPublicDir = forgeax !== null && typeof forgeax === "object" ? forgeax.assets?.publicDir : void 0;
1354
- const assetPublicDir = typeof configuredPublicDir === "string" ? configuredPublicDir : void 0;
1355
- const physics = forge.physics === "2d" || forge.physics === "3d" ? forge.physics : void 0;
1356
- const defaultScene = typeof forge.defaultScene === "string" && forge.defaultScene.length > 0 ? forge.defaultScene : void 0;
1357
- const normalizedEntry = forge.entry.startsWith("./") ? forge.entry : `./${forge.entry}`;
1358
- const bootstrapEntry = pluginModuleNames(plugins).map((name) => name.startsWith("./") ? name : `./${name}`).includes(normalizedEntry) ? void 0 : forge.entry;
1789
+ ...projectRoots,
1790
+ ...builtinPack === void 0 ? [] : [builtinPack],
1791
+ ...canonicalKit === void 0 ? [] : [canonicalKit.root]
1792
+ ];
1793
+ const importers = [...projectImporters(facts)];
1794
+ const runtimeBinding = createStandaloneRuntimeAssetBinding(facts.id);
1795
+ const engineWorkspaceResolver = await createEngineWorkspaceResolver(facts.root);
1796
+ const consumerAliases = await consumerEngineAliases(facts.root);
1797
+ const plugins = [
1798
+ ...engineWorkspaceResolver === void 0 ? [] : [engineWorkspaceResolver],
1799
+ forgeaxShader(),
1800
+ pluginPack({
1801
+ roots,
1802
+ runtimeBinding,
1803
+ ddc: devKitDdcRoots(facts.root),
1804
+ refresh: command2 === "serve" ? reloadAssetHost() : void 0,
1805
+ importers: [
1806
+ audioImporter,
1807
+ imageImporter,
1808
+ fbxImporter,
1809
+ gltfImporter,
1810
+ fontImporter,
1811
+ ...importers
1812
+ ],
1813
+ cookers: [createMaterialPackCooker(roots), createParticleCodeNativeCookerFromRoots(roots)],
1814
+ ignorePath
1815
+ })
1816
+ ];
1359
1817
  return {
1360
- ok: true,
1361
- value: {
1362
- root,
1363
- id: forge.id,
1364
- name: forge.name,
1365
- entry: forge.entry,
1366
- ...bootstrapEntry === void 0 ? {} : { bootstrapEntry },
1367
- plugins,
1368
- ...physics === void 0 ? {} : { physics },
1369
- ...defaultScene === void 0 ? {} : { defaultScene },
1370
- assetRoots,
1371
- ...assetImporters.length === 0 ? {} : { assetImporters },
1372
- ...assetPublicDir === void 0 ? {} : { assetPublicDir },
1373
- packageJson
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") }
1374
1833
  }
1375
1834
  };
1376
1835
  }
1377
- function isCommandError(value) {
1378
- if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
1379
- const candidate = value;
1380
- return typeof candidate.code === "string" && typeof candidate.expected === "string" && typeof candidate.hint === "string" && candidate.detail !== null && typeof candidate.detail === "object" && !Array.isArray(candidate.detail);
1381
- }
1382
- function commandError(cause, fallbackCode) {
1383
- if (isCommandError(cause)) return cause;
1836
+ var hostRequire;
1837
+ var init_host = __esm({
1838
+ "src/host.ts"() {
1839
+ init_engine_binding();
1840
+ hostRequire = createRequire(import.meta.url);
1841
+ }
1842
+ });
1843
+
1844
+ // src/types.ts
1845
+ function parseProjectPortOption(value, provided) {
1846
+ if (!provided) return { ok: true, value: void 0 };
1847
+ if (value !== void 0 && /^(0|[1-9]\d*)$/.test(value)) {
1848
+ const port = Number(value);
1849
+ if (Number.isSafeInteger(port) && port <= 65535) return { ok: true, value: port };
1850
+ }
1384
1851
  return {
1385
- code: fallbackCode,
1386
- expected: "the ForgeaX command to complete",
1387
- hint: "Inspect the underlying diagnostic and repair the owning input.",
1388
- detail: { reason: cause instanceof Error ? cause.message : String(cause) }
1852
+ ok: false,
1853
+ error: {
1854
+ code: "cli-parse-error",
1855
+ expected: "--port to be 0 or an integer from 1 to 65535",
1856
+ hint: "Omit --port for strict 5173, pass a positive port for a strict binding, or pass 0 for an OS-assigned port.",
1857
+ detail: { option: "--port", received: value ?? null }
1858
+ }
1389
1859
  };
1390
1860
  }
1391
- var init_project = __esm({
1392
- "src/project.ts"() {
1861
+ function resolveProjectPort(port) {
1862
+ return { port: port ?? 5173, strictPort: port !== 0 };
1863
+ }
1864
+ var init_types = __esm({
1865
+ "src/types.ts"() {
1393
1866
  }
1394
1867
  });
1395
1868
  function failure(error) {
1396
1869
  return { ok: false, error };
1397
1870
  }
1398
- function producerFailure(stderr, fallback, detail) {
1399
- try {
1400
- const parsed = JSON.parse(stderr.at(-1) ?? "");
1401
- if (isCommandError(parsed)) return failure(parsed);
1402
- } catch {
1403
- }
1404
- return failure({ ...fallback, detail: { ...detail, diagnostic: stderr.join("\n") } });
1405
- }
1406
1871
  async function sourcesAt(path) {
1407
1872
  const info = await stat(path);
1408
1873
  if (info.isFile()) return [path];
@@ -1486,15 +1951,16 @@ async function addGltf(sourcePath, dryRun2) {
1486
1951
  stderrWrite: (line) => stderr.push(line)
1487
1952
  });
1488
1953
  if (exitCode !== 0) {
1489
- return producerFailure(
1490
- stderr,
1491
- {
1954
+ try {
1955
+ return failure(JSON.parse(stderr.at(-1) ?? ""));
1956
+ } catch {
1957
+ return failure({
1492
1958
  code: "asset-add-failed",
1493
1959
  expected: "the glTF producer to create or reuse a valid sidecar",
1494
- hint: "Inspect the glTF source and its external references."
1495
- },
1496
- { source: sourcePath }
1497
- );
1960
+ hint: "Inspect the glTF source and its external references.",
1961
+ detail: { source: sourcePath, diagnostic: stderr.join("\n") }
1962
+ });
1963
+ }
1498
1964
  }
1499
1965
  return { ok: true, value: { source: sourcePath, metaPath: `${sourcePath}.meta.json` } };
1500
1966
  }
@@ -1537,15 +2003,16 @@ async function entries(options) {
1537
2003
  { stdoutWrite: (line) => stdout.push(line), stderrWrite: (line) => stderr.push(line) }
1538
2004
  );
1539
2005
  if (!result.ok) {
1540
- return producerFailure(
1541
- stderr,
1542
- {
2006
+ try {
2007
+ return failure(JSON.parse(stderr.at(-1) ?? ""));
2008
+ } catch {
2009
+ return failure({
1543
2010
  code: "asset-authority-invalid",
1544
2011
  expected: "all asset roots and sidecars to pass the pack scanner",
1545
- hint: "Repair the first invalid asset authority reported by the scanner."
1546
- },
1547
- {}
1548
- );
2012
+ hint: "Repair the first invalid asset authority reported by the scanner.",
2013
+ detail: { diagnostic: stderr.join("\n") }
2014
+ });
2015
+ }
1549
2016
  }
1550
2017
  return { ok: true, value: { facts: facts.value, entries: result.value } };
1551
2018
  }
@@ -1630,7 +2097,6 @@ var init_package = __esm({
1630
2097
  "@forgeax/engine-audio-webaudio": "workspace:*",
1631
2098
  "@forgeax/engine-fbx": "workspace:*",
1632
2099
  "@forgeax/engine-font": "workspace:*",
1633
- "@forgeax/engine-geometry": "workspace:*",
1634
2100
  "@forgeax/engine-gltf": "workspace:*",
1635
2101
  "@forgeax/engine-image": "workspace:*",
1636
2102
  "@forgeax/engine-input": "workspace:*",
@@ -1640,6 +2106,8 @@ var init_package = __esm({
1640
2106
  "@forgeax/engine-preview": "workspace:*",
1641
2107
  "@forgeax/engine-project": "workspace:*",
1642
2108
  "@forgeax/engine-rhi-debug": "workspace:*",
2109
+ "@forgeax/engine-rhi-null": "workspace:*",
2110
+ "@forgeax/engine-rhi-webgpu": "workspace:*",
1643
2111
  "@forgeax/engine-render": "workspace:*",
1644
2112
  "@forgeax/engine-render-graph": "workspace:*",
1645
2113
  "@forgeax/engine-runtime": "workspace:*",
@@ -1654,10 +2122,10 @@ var init_package = __esm({
1654
2122
  jiti: "1.21.7",
1655
2123
  playwright: "1.60.0",
1656
2124
  vite: "8.0.10",
1657
- vitest: "4.1.5"
2125
+ vitest: "4.0.18",
2126
+ webgpu: "^0.4.0"
1658
2127
  },
1659
2128
  devDependencies: {
1660
- "@forgeax/engine-rhi-null": "workspace:*",
1661
2129
  "@types/node": "^20.14.0"
1662
2130
  },
1663
2131
  forgeax: {
@@ -1829,7 +2297,7 @@ var init_init = __esm({
1829
2297
  "@webgpu/types": "0.1.71",
1830
2298
  tsx: "4.23.1",
1831
2299
  typescript: "6.0.3",
1832
- vitest: "4.1.5"
2300
+ vitest: "4.0.18"
1833
2301
  };
1834
2302
  }
1835
2303
  });
@@ -1871,6 +2339,20 @@ var init_sdk = __esm({
1871
2339
  "src/sdk.ts"() {
1872
2340
  }
1873
2341
  });
2342
+ function agentOnboarding(sdk, projectRoot2) {
2343
+ const root = projectRoot2 ?? sdk.root;
2344
+ return {
2345
+ read: [
2346
+ resolve(root, "AGENTS.md"),
2347
+ resolve(root, "skills", "forgeax-engine-sdk", "SKILL.md"),
2348
+ resolve(root, "skills", "forgeax-engine-sdk", "references", "feature-catalog.md")
2349
+ ],
2350
+ next: projectRoot2 === void 0 ? {
2351
+ cwd: root,
2352
+ argv: ["node", "./bin/forgeax.mjs", "new", "../my-game"]
2353
+ } : { cwd: root, argv: ["pnpm", "exec", "forgeax", "list", "--json"] }
2354
+ };
2355
+ }
1874
2356
  function sdkInitPath(sdk) {
1875
2357
  return resolve(sdk.root, ...SDK_INIT_PATH);
1876
2358
  }
@@ -1882,19 +2364,21 @@ function sdkProjectInstallArgs(store) {
1882
2364
  "install",
1883
2365
  "--frozen-lockfile",
1884
2366
  "--ignore-scripts",
2367
+ "--config.pm-on-fail=ignore",
1885
2368
  "--side-effects-cache=true",
1886
2369
  "--child-concurrency=1"
1887
2370
  ];
1888
- return store === void 0 ? common : [...common, "--offline", "--trust-lockfile", "--store-dir", store];
2371
+ return store === void 0 ? common : [...common, "--offline", "--config.trust-lockfile=true", "--store-dir", store];
1889
2372
  }
1890
2373
  function sdkBootstrapInstallArgs(store) {
1891
2374
  const common = [
1892
2375
  "install",
1893
2376
  "--frozen-lockfile",
2377
+ "--config.pm-on-fail=ignore",
1894
2378
  "--child-concurrency=1",
1895
2379
  "--side-effects-cache=true"
1896
2380
  ];
1897
- return store === void 0 ? common : [...common, "--offline", "--trust-lockfile", "--store-dir", store];
2381
+ return store === void 0 ? common : [...common, "--offline", "--config.trust-lockfile=true", "--store-dir", store];
1898
2382
  }
1899
2383
  function supportedPnpm(version) {
1900
2384
  const [major = 0, minor = 0] = version.split(".").map(Number);
@@ -1984,7 +2468,8 @@ async function sdkInitCommand(sdk, options = {}) {
1984
2468
  const report = {
1985
2469
  ...state,
1986
2470
  root: sdk.root,
1987
- store: sdk.store === void 0 ? "registry" : "offline"
2471
+ store: sdk.store === void 0 ? "registry" : "offline",
2472
+ onboarding: agentOnboarding(sdk)
1988
2473
  };
1989
2474
  if (options.dryRun === true || options.install === false) return { ok: true, value: report };
1990
2475
  let staging;
@@ -2019,6 +2504,107 @@ var init_sdk_bootstrap = __esm({
2019
2504
  SDK_INIT_SCHEMA_VERSION = "1.0.0";
2020
2505
  }
2021
2506
  });
2507
+
2508
+ // src/sdk-update.ts
2509
+ function parseVersion(version) {
2510
+ const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(
2511
+ version
2512
+ );
2513
+ if (match === null) return void 0;
2514
+ const major = match[1];
2515
+ const minor = match[2];
2516
+ const patch = match[3];
2517
+ if (major === void 0 || minor === void 0 || patch === void 0) return void 0;
2518
+ const prerelease = match[4]?.split(".");
2519
+ if (prerelease?.some(
2520
+ (identifier) => identifier.length === 0 || /^\d+$/.test(identifier) && /^0\d+/.test(identifier)
2521
+ )) {
2522
+ return void 0;
2523
+ }
2524
+ return {
2525
+ core: [major, minor, patch],
2526
+ ...prerelease === void 0 ? {} : { prerelease }
2527
+ };
2528
+ }
2529
+ function compareNumericIdentifier(current, latest) {
2530
+ if (current.length !== latest.length) return latest.length > current.length ? 1 : -1;
2531
+ if (current === latest) return 0;
2532
+ return latest > current ? 1 : -1;
2533
+ }
2534
+ function comparePrerelease(current, latest) {
2535
+ const length = Math.max(current.length, latest.length);
2536
+ for (let index = 0; index < length; index += 1) {
2537
+ const currentIdentifier = current[index];
2538
+ const latestIdentifier = latest[index];
2539
+ if (currentIdentifier === void 0) return 1;
2540
+ if (latestIdentifier === void 0) return -1;
2541
+ if (currentIdentifier === latestIdentifier) continue;
2542
+ const currentNumeric = /^\d+$/.test(currentIdentifier);
2543
+ const latestNumeric = /^\d+$/.test(latestIdentifier);
2544
+ if (currentNumeric && latestNumeric) {
2545
+ return compareNumericIdentifier(currentIdentifier, latestIdentifier);
2546
+ }
2547
+ if (currentNumeric !== latestNumeric) return latestNumeric ? -1 : 1;
2548
+ return latestIdentifier > currentIdentifier ? 1 : -1;
2549
+ }
2550
+ return 0;
2551
+ }
2552
+ function newerSdkVersion(currentVersion, latestVersion) {
2553
+ const current = parseVersion(currentVersion);
2554
+ const latest = parseVersion(latestVersion);
2555
+ if (current === void 0 || latest === void 0) return false;
2556
+ for (let index = 0; index < current.core.length; index += 1) {
2557
+ const precedence = compareNumericIdentifier(
2558
+ current.core[index] ?? "0",
2559
+ latest.core[index] ?? "0"
2560
+ );
2561
+ if (precedence !== 0) return precedence > 0;
2562
+ }
2563
+ if (current.prerelease !== void 0 && latest.prerelease === void 0) return true;
2564
+ if (current.prerelease === void 0 || latest.prerelease === void 0) return false;
2565
+ return comparePrerelease(current.prerelease, latest.prerelease) > 0;
2566
+ }
2567
+ function offline() {
2568
+ return ["1", "true"].includes((process.env.npm_config_offline ?? "").toLowerCase());
2569
+ }
2570
+ async function checkSdkUpdate(currentVersion) {
2571
+ if (process.env.FORGEAX_DISABLE_UPDATE_CHECK === "1") {
2572
+ return { status: "skipped", currentVersion, reason: "disabled" };
2573
+ }
2574
+ if (offline()) return { status: "skipped", currentVersion, reason: "offline" };
2575
+ try {
2576
+ const registry = process.env.npm_config_registry ?? "https://registry.npmjs.org/";
2577
+ const response = await fetch(
2578
+ new URL("@forgeax%2Fengine-sdk", `${registry.replace(/\/$/, "")}/`),
2579
+ {
2580
+ headers: { accept: "application/vnd.npm.install-v1+json" },
2581
+ signal: AbortSignal.timeout(2e3)
2582
+ }
2583
+ );
2584
+ if (!response.ok) throw new Error(`registry-http-${response.status}`);
2585
+ const metadata = await response.json();
2586
+ const latestVersion = metadata["dist-tags"]?.latest;
2587
+ if (typeof latestVersion !== "string") throw new Error("registry-latest-version-missing");
2588
+ return newerSdkVersion(currentVersion, latestVersion) ? {
2589
+ status: "available",
2590
+ currentVersion,
2591
+ latestVersion,
2592
+ migrationRisk: UPDATE_MIGRATION_RISK
2593
+ } : { status: "current", currentVersion, latestVersion };
2594
+ } catch (cause) {
2595
+ return {
2596
+ status: "unavailable",
2597
+ currentVersion,
2598
+ reason: cause instanceof Error ? cause.message : String(cause)
2599
+ };
2600
+ }
2601
+ }
2602
+ var UPDATE_MIGRATION_RISK;
2603
+ var init_sdk_update = __esm({
2604
+ "src/sdk-update.ts"() {
2605
+ UPDATE_MIGRATION_RISK = "Install a newer SDK in a separate directory. Existing games remain pinned and are not migrated automatically; review release notes and migrate and test each game before changing its Engine version.";
2606
+ }
2607
+ });
2022
2608
  function slash(path) {
2023
2609
  return path.split(sep).join("/");
2024
2610
  }
@@ -2646,9 +3232,16 @@ async function newCommand(options = {}) {
2646
3232
  env: { ...process.env, CI: "true" },
2647
3233
  maxBuffer: 16 * 1024 * 1024
2648
3234
  });
3235
+ const sdkUpdate = await checkSdkUpdate(sdk.manifest.sdkVersion);
2649
3236
  return {
2650
3237
  ok: true,
2651
- value: { root, template: templateId, sdkVersion: sdk.manifest.sdkVersion }
3238
+ value: {
3239
+ root,
3240
+ template: templateId,
3241
+ sdkVersion: sdk.manifest.sdkVersion,
3242
+ onboarding: agentOnboarding(sdk, root),
3243
+ sdkUpdate
3244
+ }
2652
3245
  };
2653
3246
  } catch (cause) {
2654
3247
  if (staging !== void 0) await rm(staging, { recursive: true, force: true });
@@ -2681,6 +3274,7 @@ var init_bootstrap_commands = __esm({
2681
3274
  init_project();
2682
3275
  init_sdk();
2683
3276
  init_sdk_bootstrap();
3277
+ init_sdk_update();
2684
3278
  init_skill_install();
2685
3279
  execFileAsync2 = promisify(execFile);
2686
3280
  }
@@ -2692,7 +3286,7 @@ __export(plugin_authoring_exports, {
2692
3286
  pluginInstallCommand: () => pluginInstallCommand,
2693
3287
  pluginUninstallCommand: () => pluginUninstallCommand
2694
3288
  });
2695
- async function readManifest(root) {
3289
+ async function readManifest2(root) {
2696
3290
  const path = resolve(root, "forge.json");
2697
3291
  try {
2698
3292
  const raw = await readFile(path, "utf8");
@@ -2745,7 +3339,7 @@ async function mutateDependency(root, action, dependency) {
2745
3339
  }
2746
3340
  async function pluginInstallCommand(options) {
2747
3341
  const root = resolve(options.root ?? process.cwd());
2748
- const manifest = await readManifest(root);
3342
+ const manifest = await readManifest2(root);
2749
3343
  if (!manifest.ok) return manifest;
2750
3344
  const parsed = GameProjectSchema.safeParse(manifest.value.value);
2751
3345
  if (!parsed.success) {
@@ -2798,7 +3392,7 @@ async function pluginInstallCommand(options) {
2798
3392
  }
2799
3393
  async function pluginUninstallCommand(options) {
2800
3394
  const root = resolve(options.root ?? process.cwd());
2801
- const manifest = await readManifest(root);
3395
+ const manifest = await readManifest2(root);
2802
3396
  if (!manifest.ok) return manifest;
2803
3397
  const parsed = GameProjectSchema.safeParse(manifest.value.value);
2804
3398
  if (!parsed.success) {
@@ -2849,491 +3443,12 @@ var init_plugin_authoring = __esm({
2849
3443
  execFileAsync3 = promisify(execFile);
2850
3444
  }
2851
3445
  });
2852
- var RhiError;
2853
- var init_dist2 = __esm({
2854
- "../rhi/dist/index.mjs"() {
2855
- RhiError = class extends Error {
2856
- code;
2857
- expected;
2858
- hint;
2859
- detail;
2860
- constructor(args2) {
2861
- super(`[RhiError ${args2.code}] expected: ${args2.expected}; hint: ${args2.hint}`);
2862
- this.name = "RhiError";
2863
- this.code = args2.code;
2864
- this.expected = args2.expected;
2865
- this.hint = args2.hint;
2866
- this.detail = args2.detail;
2867
- }
2868
- };
2869
- }
2870
- });
2871
- function readPassLabel(desc) {
2872
- if (desc && typeof desc.label === "string" && desc.label.length > 0) {
2873
- return desc.label;
2874
- }
2875
- return "<unnamed>";
2876
- }
2877
- function readRecord(handle) {
2878
- if (handle === null || typeof handle !== "object") return void 0;
2879
- const tagged = handle;
2880
- return tagged[BOOKKEEPING_KEY];
2881
- }
2882
- function isHandleDestroyed(handle) {
2883
- if (readRecord(handle)?.destroyed === true) {
2884
- return err(
2885
- new RhiError({
2886
- code: "destroy-after-destroy",
2887
- expected: "GPU buffer/texture handle has not been destroyed yet",
2888
- hint: "object already destroyed; track lifecycle in caller or check isDestroyed before re-destroy"
2889
- })
2890
- );
2891
- }
2892
- return ok(void 0);
2893
- }
2894
- function acquireCanvasContext(_canvas) {
2895
- return ok(new RhiNullCanvasContext());
2896
- }
2897
- function createShaderModule(_device, _desc) {
2898
- return Promise.resolve(ok({}));
2899
- }
2900
- function requestAdapter(_opts, _compatibleSurface) {
2901
- return Promise.resolve(ok(new RhiNullAdapter()));
2902
- }
2903
- var RhiNullRenderPassEncoder, RhiNullComputePassEncoder, DeviceCounter, RhiNullCommandEncoder, BOOKKEEPING_KEY, Bookkeeper, nextDeviceId, RhiNullDevice, EMPTY_FEATURES, EMPTY_LIMITS, NEVER, RhiNullQueue, RhiNullAdapter, RhiNullCanvasContext, rhi;
2904
- var init_dist3 = __esm({
2905
- "../rhi-null/dist/index.mjs"() {
2906
- init_dist2();
2907
- RhiNullRenderPassEncoder = class {
2908
- /** Number of draw* calls issued on this pass (AC-06 readback). */
2909
- drawCount = 0;
2910
- bindGroupCount = 0;
2911
- /** Most recent setVertexBuffer / setBindGroup ownership validation; ok unless
2912
- * a foreign handle was passed (AC-09 readback). */
2913
- lastValidation = ok(void 0);
2914
- bookkeeper;
2915
- counter;
2916
- passName;
2917
- constructor(bookkeeper, counter, passName) {
2918
- this.bookkeeper = bookkeeper;
2919
- this.counter = counter;
2920
- this.passName = passName;
2921
- }
2922
- setPipeline(_pipeline) {
2923
- }
2924
- setVertexBuffer(_slot, buffer, _offset, _size) {
2925
- this.lastValidation = this.bookkeeper.validateOwnership(buffer);
2926
- }
2927
- setIndexBuffer(_buffer, _format, _offset, _size) {
2928
- }
2929
- setBindGroup(_index, bindGroup, _dynamicOffsetsData, _dynamicOffsetsDataStart, _dynamicOffsetsDataLength) {
2930
- this.bindGroupCount++;
2931
- this.counter?.recordBindGroup();
2932
- this.lastValidation = this.bookkeeper.validateOwnership(bindGroup);
2933
- }
2934
- draw(_vertexCount, _instanceCount, _firstVertex, _firstInstance) {
2935
- this.drawCount++;
2936
- this.counter?.recordDraw();
2937
- }
2938
- drawIndexed(_indexCount, _instanceCount, _firstIndex, _baseVertex, _firstInstance) {
2939
- this.drawCount++;
2940
- this.counter?.recordDraw();
2941
- }
2942
- end() {
2943
- this.counter?.recordPassName(this.passName);
2944
- }
2945
- setViewport(_x, _y, _w, _h, _minDepth, _maxDepth) {
2946
- }
2947
- setScissorRect(_x, _y, _w, _h) {
2948
- }
2949
- setBlendConstant(_color) {
2950
- }
2951
- setStencilReference(_reference) {
2952
- }
2953
- drawIndirect(_indirectBuffer, _indirectOffset) {
2954
- this.drawCount++;
2955
- this.counter?.recordDraw();
2956
- }
2957
- drawIndexedIndirect(_indirectBuffer, _indirectOffset) {
2958
- this.drawCount++;
2959
- this.counter?.recordDraw();
2960
- }
2961
- pushDebugGroup(_groupLabel) {
2962
- }
2963
- popDebugGroup() {
2964
- }
2965
- insertDebugMarker(_markerLabel) {
2966
- }
2967
- executeBundles(_bundles) {
2968
- return ok(void 0);
2969
- }
2970
- beginOcclusionQuery(_queryIndex) {
2971
- return ok(void 0);
2972
- }
2973
- endOcclusionQuery() {
2974
- return ok(void 0);
2975
- }
2976
- };
2977
- RhiNullComputePassEncoder = class {
2978
- /** Number of dispatchWorkgroups calls issued on this pass (readback). */
2979
- dispatchCount = 0;
2980
- /** Most recent setBindGroup ownership validation (AC-09 readback). */
2981
- lastValidation = ok(void 0);
2982
- bookkeeper;
2983
- counter;
2984
- passName;
2985
- constructor(bookkeeper, counter, passName) {
2986
- this.bookkeeper = bookkeeper;
2987
- this.counter = counter;
2988
- this.passName = passName;
2989
- }
2990
- setPipeline(_pipeline) {
2991
- }
2992
- setBindGroup(_index, bindGroup, _dynamicOffsets) {
2993
- this.lastValidation = this.bookkeeper.validateOwnership(bindGroup);
2994
- }
2995
- dispatchWorkgroups(_x, _y, _z) {
2996
- this.dispatchCount++;
2997
- this.counter?.recordDispatch();
2998
- }
2999
- dispatchWorkgroupsIndirect(indirectBuffer, _indirectOffset) {
3000
- this.lastValidation = this.bookkeeper.validateOwnership(indirectBuffer);
3001
- this.dispatchCount++;
3002
- this.counter?.recordDispatch();
3003
- }
3004
- end() {
3005
- this.counter?.recordPassName(this.passName);
3006
- }
3007
- };
3008
- DeviceCounter = class {
3009
- constructor(device) {
3010
- this.device = device;
3011
- }
3012
- device;
3013
- recordDraw() {
3014
- this.device.totalDrawCount++;
3015
- }
3016
- recordDispatch() {
3017
- this.device.totalDispatchCount++;
3018
- }
3019
- recordBindGroup() {
3020
- this.device.totalBindGroupCount++;
3021
- }
3022
- recordPassName(name) {
3023
- this.device.framePassNames.push(name);
3024
- }
3025
- };
3026
- RhiNullCommandEncoder = class {
3027
- bookkeeper;
3028
- counter;
3029
- constructor(bookkeeper, device) {
3030
- this.bookkeeper = bookkeeper;
3031
- this.counter = new DeviceCounter(device);
3032
- }
3033
- beginRenderPass(desc) {
3034
- const label = readPassLabel(desc);
3035
- return new RhiNullRenderPassEncoder(this.bookkeeper, this.counter, label);
3036
- }
3037
- beginComputePass(desc) {
3038
- const label = readPassLabel(desc);
3039
- return new RhiNullComputePassEncoder(this.bookkeeper, this.counter, label);
3040
- }
3041
- copyBufferToBuffer(_source, _sourceOffsetOrDestination, _destinationOrSize, _destinationOffset, _size) {
3042
- }
3043
- copyBufferToTexture(_source, _destination, _copySize) {
3044
- }
3045
- copyTextureToBuffer(_source, _destination, _copySize) {
3046
- }
3047
- copyTextureToTexture(_source, _destination, _copySize) {
3048
- }
3049
- clearBuffer(_buffer, _offset, _size) {
3050
- }
3051
- resolveQuerySet(_querySet, _firstQuery, _queryCount, _destination, _destinationOffset) {
3052
- return ok(void 0);
3053
- }
3054
- writeTimestamp(_querySet, _queryIndex) {
3055
- }
3056
- pushDebugGroup(_groupLabel) {
3057
- }
3058
- popDebugGroup() {
3059
- }
3060
- insertDebugMarker(_markerLabel) {
3061
- }
3062
- finish() {
3063
- return ok(this.bookkeeper.register("CommandBuffer"));
3064
- }
3065
- };
3066
- BOOKKEEPING_KEY = /* @__PURE__ */ Symbol("forgeax-rhi-null-bookkeeping");
3067
- Bookkeeper = class {
3068
- deviceId;
3069
- nextHandleId = 0;
3070
- records = /* @__PURE__ */ new Map();
3071
- constructor(deviceId) {
3072
- this.deviceId = deviceId;
3073
- }
3074
- /**
3075
- * Register a freshly-minted handle of the given kind, returning a plain
3076
- * object that carries its ledger row. The caller casts the return value to
3077
- * the concrete brand (`as unknown as Buffer` etc.).
3078
- */
3079
- register(kind) {
3080
- const id = this.nextHandleId++;
3081
- const record = {
3082
- id,
3083
- kind,
3084
- destroyed: false,
3085
- sourceDeviceId: this.deviceId
3086
- };
3087
- this.records.set(id, record);
3088
- return { [BOOKKEEPING_KEY]: record };
3089
- }
3090
- /**
3091
- * Mark a handle destroyed. Fail-fasts on a second destroy
3092
- * ('destroy-after-destroy') or on a handle issued by a different device
3093
- * ('rhi-not-available'); otherwise flips the destroyed flag and returns ok.
3094
- */
3095
- destroy(handle) {
3096
- const ownership = this.validateOwnership(handle);
3097
- if (!ownership.ok) return ownership;
3098
- const destroyedCheck = isHandleDestroyed(handle);
3099
- if (!destroyedCheck.ok) return destroyedCheck;
3100
- ownership.value.destroyed = true;
3101
- return ok(void 0);
3102
- }
3103
- /**
3104
- * Report whether a handle has already been destroyed (true once destroy()
3105
- * has flipped its flag). Used by command-stream methods that must not
3106
- * consume a stale handle.
3107
- */
3108
- isDestroyed(handle) {
3109
- return readRecord(handle)?.destroyed === true;
3110
- }
3111
- /**
3112
- * Validate that a handle was issued by THIS device. Returns the ledger row on
3113
- * success so callers can mutate it (e.g. flip destroyed); returns
3114
- * 'rhi-not-available' for a foreign handle (AC-09 — no silent pass).
3115
- */
3116
- validateOwnership(handle) {
3117
- const record = readRecord(handle);
3118
- if (record === void 0 || record.sourceDeviceId !== this.deviceId) {
3119
- return err(
3120
- new RhiError({
3121
- code: "rhi-not-available",
3122
- expected: "handle was issued by this RhiNull device",
3123
- hint: "do not pass a handle created on a different RhiNull device into this device; create resources on the device they are used with"
3124
- })
3125
- );
3126
- }
3127
- return ok(record);
3128
- }
3129
- /**
3130
- * Return all ledger rows as a readonly array. M3 unit tests (w17) read this
3131
- * to assert create/destroy pairing, BGL/PSO shape counts, and resource
3132
- * lifecycle coverage (AC-05/06/07). Returns a snapshot of the current Map
3133
- * so callers can iterate without a stale reference after further mutations.
3134
- */
3135
- allRecords() {
3136
- return [...this.records.values()];
3137
- }
3138
- /** Report the total number of records in the ledger. */
3139
- recordCount() {
3140
- return this.records.size;
3141
- }
3142
- };
3143
- nextDeviceId = 0;
3144
- RhiNullDevice = class {
3145
- internalBookkeeper;
3146
- nullQueue;
3147
- encoderFactory;
3148
- /** Per-frame total draw count across all pass encoders executed this frame
3149
- * (aggregated by the command encoder on finish, then reset). M3 unit tests
3150
- * (w17) read this to assert draw count >= 1 (AC-06). */
3151
- totalDrawCount = 0;
3152
- /** Per-frame total direct and indirect compute dispatch count. */
3153
- totalDispatchCount = 0;
3154
- /** Per-frame total bind group set count (AC-06 / AC-05 readback). */
3155
- totalBindGroupCount = 0;
3156
- /** Per-frame pass names executed this frame, in schedule order (AC-04). */
3157
- framePassNames = [];
3158
- /** The per-device handle ledger — exposed so M3 tests can assert create/destroy
3159
- * pairing and BGL/PSO shape counts (AC-05/06/07). */
3160
- get bookkeeper() {
3161
- return this.internalBookkeeper;
3162
- }
3163
- constructor(queue, encoderFactory) {
3164
- this.internalBookkeeper = new Bookkeeper(nextDeviceId++);
3165
- this.nullQueue = queue;
3166
- this.encoderFactory = encoderFactory;
3167
- }
3168
- get caps() {
3169
- return {
3170
- backendKind: "null",
3171
- compute: true,
3172
- timestampQuery: false,
3173
- timestampPeriodNanoseconds: null,
3174
- indirectDrawing: true,
3175
- textureCompressionBc: false,
3176
- textureCompressionEtc2: false,
3177
- textureCompressionAstc: false,
3178
- // 3 wgpu-native-only reserved flags stay false on non-native backends
3179
- // (D-5); the headless backend is not a native runtime.
3180
- multiDrawIndirect: false,
3181
- pushConstants: false,
3182
- textureBindingArray: false,
3183
- samplerAliasing: true,
3184
- firstInstanceIndirect: true,
3185
- storageBuffer: true,
3186
- storageTexture: true,
3187
- rgba16floatRenderable: true,
3188
- rg11b10ufloatRenderable: true,
3189
- float32Filterable: true,
3190
- maxColorAttachments: 8
3191
- };
3192
- }
3193
- get features() {
3194
- return EMPTY_FEATURES;
3195
- }
3196
- get limits() {
3197
- return EMPTY_LIMITS;
3198
- }
3199
- get queue() {
3200
- return this.nullQueue;
3201
- }
3202
- // forgeax-async-whitelist: dom-native — spec `GPUDevice.lost` Promise
3203
- // passthrough. The headless backend never loses a device (no GPU), so the
3204
- // Promise stays unsettled for the lifetime of the device, mirroring a live
3205
- // device that never transitions to the lost state.
3206
- get lost() {
3207
- return NEVER;
3208
- }
3209
- createBuffer(_desc) {
3210
- return ok(this.internalBookkeeper.register("Buffer"));
3211
- }
3212
- createTexture(_desc) {
3213
- return ok(this.internalBookkeeper.register("Texture"));
3214
- }
3215
- destroyBuffer(buf) {
3216
- return this.internalBookkeeper.destroy(buf);
3217
- }
3218
- destroyQuerySet(querySet) {
3219
- return this.internalBookkeeper.destroy(querySet);
3220
- }
3221
- destroyTexture(tex) {
3222
- return this.internalBookkeeper.destroy(tex);
3223
- }
3224
- createTextureView(_texture, _desc) {
3225
- return ok(this.internalBookkeeper.register("TextureView"));
3226
- }
3227
- createSampler(_desc) {
3228
- return ok(this.internalBookkeeper.register("Sampler"));
3229
- }
3230
- createBindGroupLayout(_desc) {
3231
- return ok(this.internalBookkeeper.register("BindGroupLayout"));
3232
- }
3233
- createBindGroup(_desc) {
3234
- return ok(this.internalBookkeeper.register("BindGroup"));
3235
- }
3236
- createPipelineLayout(_desc) {
3237
- return ok(this.internalBookkeeper.register("PipelineLayout"));
3238
- }
3239
- createRenderPipeline(_desc) {
3240
- return ok(this.makePipeline("RenderPipeline"));
3241
- }
3242
- createComputePipeline(_desc) {
3243
- return ok(this.makePipeline("ComputePipeline"));
3244
- }
3245
- createQuerySet(desc) {
3246
- if (desc.type === "timestamp") {
3247
- return err(
3248
- new RhiError({
3249
- code: "feature-not-enabled",
3250
- expected: "caps.timestampQuery === true (timestamp-query feature)",
3251
- hint: "RhiNull is structural-only and cannot produce GPU timestamp ticks"
3252
- })
3253
- );
3254
- }
3255
- return ok(this.internalBookkeeper.register("QuerySet"));
3256
- }
3257
- createCommandEncoder(_desc) {
3258
- return ok(this.encoderFactory(this.internalBookkeeper, this));
3259
- }
3260
- /**
3261
- * Mint a pipeline handle whose object also carries the no-op
3262
- * `getBindGroupLayout(index)` ops method (D-2). The prod auto-layout path
3263
- * (debug-draw.ts) and the existing mock unit tests both call
3264
- * `pipeline.getBindGroupLayout(n)`; returning a legal BindGroupLayout brand
3265
- * (recorded in the ledger) keeps those consumers from crashing on a missing
3266
- * method.
3267
- */
3268
- makePipeline(kind) {
3269
- const handle = this.internalBookkeeper.register(kind);
3270
- const getBindGroupLayout = (_index) => this.internalBookkeeper.register("BindGroupLayout");
3271
- return Object.assign(handle, { getBindGroupLayout });
3272
- }
3273
- };
3274
- EMPTY_FEATURES = /* @__PURE__ */ new Set();
3275
- EMPTY_LIMITS = {};
3276
- NEVER = new Promise(() => {
3277
- });
3278
- RhiNullQueue = class {
3279
- writeBuffer(_buffer, _bufferOffset, _data, _dataOffset, _size) {
3280
- return ok(void 0);
3281
- }
3282
- writeTexture(_destination, _data, _dataLayout, _size) {
3283
- return ok(void 0);
3284
- }
3285
- copyExternalImageToTexture(_source, _destination, _copySize) {
3286
- return ok(void 0);
3287
- }
3288
- submit(_commandBuffers) {
3289
- return ok(void 0);
3290
- }
3291
- // forgeax-async-whitelist: dom-native — spec `GPUQueue.onSubmittedWorkDone`
3292
- // never rejects. The headless backend has no pending GPU work, so it resolves
3293
- // immediately (AC-12: read-back idioms must not hang).
3294
- onSubmittedWorkDone() {
3295
- return Promise.resolve(void 0);
3296
- }
3297
- };
3298
- RhiNullAdapter = class {
3299
- features = /* @__PURE__ */ new Set();
3300
- limits = {};
3301
- // forgeax-async-whitelist is not needed: this returns Promise<Result<...>>
3302
- // per the spec contract; never rejects.
3303
- requestDevice(_opts) {
3304
- const device = new RhiNullDevice(
3305
- new RhiNullQueue(),
3306
- (bookkeeper, dev) => new RhiNullCommandEncoder(bookkeeper, dev)
3307
- );
3308
- return Promise.resolve(ok(device));
3309
- }
3310
- };
3311
- RhiNullCanvasContext = class {
3312
- configure(_desc) {
3313
- return ok(void 0);
3314
- }
3315
- unconfigure() {
3316
- }
3317
- getConfiguration() {
3318
- return void 0;
3319
- }
3320
- getCurrentTexture() {
3321
- return ok({});
3322
- }
3323
- };
3324
- rhi = {
3325
- requestAdapter,
3326
- acquireCanvasContext,
3327
- createShaderModule
3328
- };
3329
- }
3330
- });
3331
3446
  function createCliRhiDebugOperationContext() {
3332
3447
  return {
3333
3448
  captureFrame: async () => err(
3334
3449
  createRhiDebugError("capture-unavailable", {
3335
3450
  stage: "capture",
3336
- cause: "the standalone CLI has no live App capture provider"
3451
+ cause: "the standalone CLI has no live App capture provider; start a recorder-enabled live host and invoke its rhiCapture root"
3337
3452
  })
3338
3453
  ),
3339
3454
  async readArtifact(artifact) {
@@ -3358,33 +3473,69 @@ function createCliRhiDebugOperationContext() {
3358
3473
  expected: "the ArtifactRef path to be readable",
3359
3474
  hint: "Check the tape path and recapture if the artifact was removed.",
3360
3475
  detail: {
3361
- path: artifact.path,
3362
- reason: cause instanceof Error ? cause.message : String(cause)
3476
+ path: artifact.path,
3477
+ reason: cause instanceof Error ? cause.message : String(cause)
3478
+ }
3479
+ }
3480
+ };
3481
+ }
3482
+ },
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)
3363
3500
  }
3364
3501
  }
3365
3502
  };
3366
3503
  }
3367
- },
3368
- async createReplayBackend() {
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
+ });
3369
3518
  const adapter = await rhi.requestAdapter();
3370
3519
  if (!adapter.ok) {
3371
3520
  return {
3372
3521
  ok: false,
3373
3522
  error: {
3374
3523
  code: "replay-backend-unavailable",
3375
- expected: "a fresh rhi-null adapter",
3524
+ expected: "a fresh Dawn WebGPU adapter",
3376
3525
  hint: adapter.error.hint,
3377
3526
  detail: { stage: "adapter", cause: adapter.error.hint }
3378
3527
  }
3379
3528
  };
3380
3529
  }
3381
- const device = await adapter.value.requestDevice();
3530
+ const device = await adapter.value.requestDevice(
3531
+ replayDeviceRequest(tape, adapter.value.features, adapter.value.limits)
3532
+ );
3382
3533
  if (!device.ok) {
3383
3534
  return {
3384
3535
  ok: false,
3385
3536
  error: {
3386
3537
  code: "replay-backend-unavailable",
3387
- expected: "a fresh rhi-null device",
3538
+ expected: "a fresh Dawn WebGPU device satisfying the recorded tape",
3388
3539
  hint: device.error.hint,
3389
3540
  detail: { stage: "device", cause: device.error.hint }
3390
3541
  }
@@ -3396,7 +3547,6 @@ function createCliRhiDebugOperationContext() {
3396
3547
  }
3397
3548
  var init_cli_context = __esm({
3398
3549
  "src/rhi-debug/cli-context.ts"() {
3399
- init_dist3();
3400
3550
  }
3401
3551
  });
3402
3552
  function createRhiDebugOperationContext(host) {
@@ -3423,6 +3573,9 @@ function renderRhiDebugHelp() {
3423
3573
  return [
3424
3574
  "forgeax run <operation>",
3425
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",
3426
3579
  "ArtifactRef schema:",
3427
3580
  JSON.stringify(RHI_DEBUG_OPERATION_MANIFEST.artifactRefSchema)
3428
3581
  ].join("\n");
@@ -3550,7 +3703,7 @@ async function runRhiDebugOperation(name, input, context) {
3550
3703
  "Provide a fresh device and shader factory before running rhi.inspect."
3551
3704
  );
3552
3705
  }
3553
- const backend = await context.createReplayBackend();
3706
+ const backend = await context.createReplayBackend(decoded.value);
3554
3707
  if (!backend.ok) return backend;
3555
3708
  const opened = await openReplay(decoded.value, backend.value);
3556
3709
  if (!opened.ok) return { ok: false, error: coreError(opened.error) };
@@ -3642,7 +3795,10 @@ var init_operations = __esm({
3642
3795
  properties: {
3643
3796
  artifact: artifactRefSchema,
3644
3797
  workIndex: { type: "integer", minimum: 0 },
3645
- fields: { type: "array", items: { type: "string" } }
3798
+ fields: {
3799
+ type: "array",
3800
+ items: { type: "string", enum: ["bindings", "pipeline", "pixels"] }
3801
+ }
3646
3802
  },
3647
3803
  required: ["artifact", "workIndex"],
3648
3804
  additionalProperties: false
@@ -3734,7 +3890,15 @@ async function sdkInstallCommand(options) {
3734
3890
  await rename(resolve(staging, name), resolve(root, name));
3735
3891
  }
3736
3892
  }
3737
- return { ok: true, value: { root, sdkVersion: version, source: "@forgeax/engine-sdk" } };
3893
+ return {
3894
+ ok: true,
3895
+ value: {
3896
+ root,
3897
+ sdkVersion: version,
3898
+ source: "@forgeax/engine-sdk",
3899
+ next: { cwd: root, argv: ["node", "./bin/forgeax.mjs", "init"] }
3900
+ }
3901
+ };
3738
3902
  } catch (cause) {
3739
3903
  return { ok: false, error: commandError(cause, "sdk-install-failed") };
3740
3904
  } finally {
@@ -3783,6 +3947,630 @@ var init_shader_check = __esm({
3783
3947
  init_project();
3784
3948
  }
3785
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
+ });
3786
4574
 
3787
4575
  // src/commands.ts
3788
4576
  var commands_exports = {};
@@ -3792,12 +4580,17 @@ __export(commands_exports, {
3792
4580
  assetInspectCommand: () => assetInspectCommand,
3793
4581
  assetListCommand: () => assetListCommand,
3794
4582
  assetVerifyCommand: () => assetVerifyCommand,
4583
+ browserCaptureCommand: () => browserCaptureCommand,
3795
4584
  buildCommand: () => buildCommand,
3796
4585
  createCliRhiDebugOperationContext: () => createCliRhiDebugOperationContext,
3797
4586
  createRhiDebugOperationContext: () => createRhiDebugOperationContext,
3798
4587
  devCommand: () => devCommand,
3799
4588
  discoverRhiDebugOperations: () => discoverRhiDebugOperations,
3800
4589
  doctorCommand: () => doctorCommand,
4590
+ engineDoctorCommand: () => engineDoctorCommand,
4591
+ engineStatusCommand: () => engineStatusCommand,
4592
+ engineUnlinkCommand: () => engineUnlinkCommand,
4593
+ engineUseLocalCommand: () => engineUseLocalCommand,
3801
4594
  initCommand: () => initCommand,
3802
4595
  newCommand: () => newCommand,
3803
4596
  packageCommand: () => packageCommand,
@@ -3812,11 +4605,27 @@ __export(commands_exports, {
3812
4605
  shaderCheckCommand: () => shaderCheckCommand,
3813
4606
  skillInstallCommand: () => skillInstallCommand,
3814
4607
  skillVerifyCommand: () => skillVerifyCommand,
4608
+ softwareCaptureCommand: () => softwareCaptureCommand,
3815
4609
  testCommand: () => testCommand
3816
4610
  });
3817
4611
  function runRhiDebugCommand(name, input, context) {
3818
4612
  return runRhiDebugOperation(name, input, context);
3819
4613
  }
4614
+ async function materializeViteDevPort(port) {
4615
+ if (port !== 0) return port;
4616
+ const reservation = createServer();
4617
+ await new Promise((resolveListen, rejectListen) => {
4618
+ reservation.once("error", rejectListen);
4619
+ reservation.listen(0, "127.0.0.1", resolveListen);
4620
+ });
4621
+ const address = reservation.address();
4622
+ const assigned = typeof address === "object" && address !== null ? address.port : 0;
4623
+ await new Promise((resolveClose, rejectClose) => {
4624
+ reservation.close((error) => error === void 0 ? resolveClose() : rejectClose(error));
4625
+ });
4626
+ if (assigned === 0) throw new Error("OS did not assign an ephemeral preview port");
4627
+ return assigned;
4628
+ }
3820
4629
  async function buildCommand(options = {}) {
3821
4630
  const facts = await readProjectFacts(options.root);
3822
4631
  if (!facts.ok) return facts;
@@ -3919,13 +4728,38 @@ async function devCommand(options = {}) {
3919
4728
  const facts = await readProjectFacts(options.root);
3920
4729
  if (!facts.ok) return facts;
3921
4730
  const previous = process.cwd();
4731
+ let server;
3922
4732
  try {
4733
+ const { createServer: createServer2 } = await import('vite');
4734
+ const { createViteConfig: createViteConfig2 } = await Promise.resolve().then(() => (init_host(), host_exports));
4735
+ const port = resolveProjectPort(options.port);
4736
+ const vitePort = { ...port, port: await materializeViteDevPort(port.port) };
3923
4737
  process.chdir(facts.value.root);
3924
- const server = await createServer(await createViteConfig(facts.value, "serve"));
3925
- await server.listen();
4738
+ server = await createServer2(
4739
+ await createViteConfig2(facts.value, "serve", "/", {
4740
+ server: vitePort
4741
+ })
4742
+ );
4743
+ await server.listen(vitePort.port);
3926
4744
  if (options.json !== true) server.printUrls();
3927
- return { ok: true, value: { root: facts.value.root, urls: server.resolvedUrls } };
4745
+ return {
4746
+ ok: true,
4747
+ value: {
4748
+ root: facts.value.root,
4749
+ urls: server.resolvedUrls,
4750
+ mode: "dev",
4751
+ serves: "source",
4752
+ capabilities: {
4753
+ "rhi.capture": {
4754
+ available: false,
4755
+ realm: "host",
4756
+ reason: "standalone-dev-server-has-no-live-app-cli-attachment"
4757
+ }
4758
+ }
4759
+ }
4760
+ };
3928
4761
  } catch (cause) {
4762
+ await server?.close();
3929
4763
  process.chdir(previous);
3930
4764
  return { ok: false, error: commandError(cause, "dev-server-failed") };
3931
4765
  }
@@ -3935,15 +4769,31 @@ async function previewCommand(options = {}) {
3935
4769
  const verified = await verifyDist(resolve(root, "dist"));
3936
4770
  if (!verified.ok) return verified;
3937
4771
  try {
4772
+ const port = resolveProjectPort(options.port);
3938
4773
  const server = await preview({
3939
4774
  root,
3940
4775
  configFile: false,
3941
4776
  base: verified.value.base,
3942
- preview: { open: false, port: 0, strictPort: false },
4777
+ preview: { open: false, ...port },
3943
4778
  build: { outDir: resolve(root, "dist") }
3944
4779
  });
3945
4780
  if (options.json !== true) server.printUrls();
3946
- return { ok: true, value: { root, urls: server.resolvedUrls } };
4781
+ return {
4782
+ ok: true,
4783
+ value: {
4784
+ root,
4785
+ urls: server.resolvedUrls,
4786
+ mode: "preview",
4787
+ serves: "dist",
4788
+ capabilities: {
4789
+ "rhi.capture": {
4790
+ available: false,
4791
+ realm: "host",
4792
+ reason: "static-dist-host-does-not-install-dev-capture"
4793
+ }
4794
+ }
4795
+ }
4796
+ };
3947
4797
  } catch (cause) {
3948
4798
  return { ok: false, error: commandError(cause, "preview-server-failed") };
3949
4799
  }
@@ -3993,14 +4843,17 @@ var init_commands = __esm({
3993
4843
  init_dist();
3994
4844
  init_host();
3995
4845
  init_project();
4846
+ init_types();
3996
4847
  init_assets();
3997
4848
  init_bootstrap_commands();
4849
+ init_engine_binding();
3998
4850
  init_plugin_authoring();
3999
4851
  init_cli_context();
4000
4852
  init_operations();
4001
4853
  init_sdk_install();
4002
4854
  init_shader_check();
4003
4855
  init_skill_install();
4856
+ init_software_capture();
4004
4857
  init_operations();
4005
4858
  }
4006
4859
  });
@@ -4365,7 +5218,7 @@ function hasStaticToolDeclarations(entry) {
4365
5218
  return Array.isArray(Reflect.get(entry.config, "tools"));
4366
5219
  }
4367
5220
  async function createViteModuleLoader(root) {
4368
- const server = await createServer({
5221
+ const server = await createServer$1({
4369
5222
  root,
4370
5223
  appType: "custom",
4371
5224
  configFile: false,
@@ -4622,9 +5475,42 @@ var init_runtime = __esm({
4622
5475
  }
4623
5476
  });
4624
5477
 
5478
+ // src/cli-output.ts
5479
+ function agentOnboardingLines(value) {
5480
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return [];
5481
+ const onboarding = value.onboarding;
5482
+ if (onboarding === null || typeof onboarding !== "object" || Array.isArray(onboarding)) return [];
5483
+ const candidate = onboarding;
5484
+ const lines = Array.isArray(candidate.read) ? candidate.read.filter((path) => typeof path === "string").map((path) => `[forgeax] read: ${path}`) : [];
5485
+ if (typeof candidate.next?.cwd === "string" && Array.isArray(candidate.next.argv) && candidate.next.argv.every((part) => typeof part === "string")) {
5486
+ lines.push(`[forgeax] next cwd: ${candidate.next.cwd}`);
5487
+ lines.push(`[forgeax] next: ${candidate.next.argv.join(" ")}`);
5488
+ }
5489
+ return lines;
5490
+ }
5491
+ function sdkUpdateLines(value) {
5492
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return [];
5493
+ const update = value.sdkUpdate;
5494
+ if (update === null || typeof update !== "object" || Array.isArray(update)) return [];
5495
+ const candidate = update;
5496
+ if (candidate.status !== "available" || typeof candidate.currentVersion !== "string" || typeof candidate.latestVersion !== "string" || typeof candidate.migrationRisk !== "string") {
5497
+ return [];
5498
+ }
5499
+ return [
5500
+ `[forgeax] update available: @forgeax/engine-sdk ${candidate.currentVersion} -> ${candidate.latestVersion}`,
5501
+ `[forgeax] update note: ${candidate.migrationRisk}`
5502
+ ];
5503
+ }
5504
+ function renderForgeaxUsage() {
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";
5506
+ }
5507
+
4625
5508
  // src/cli.ts
4626
5509
  init_commands();
4627
5510
 
5511
+ // src/tools/commands.ts
5512
+ init_software_capture();
5513
+
4628
5514
  // src/tools/client.ts
4629
5515
  init_catalog();
4630
5516
  function decorateResourcePreviewTerminal(terminal) {
@@ -4795,6 +5681,7 @@ async function execCommand(options) {
4795
5681
  };
4796
5682
  }
4797
5683
  const root = options.root ?? process.cwd();
5684
+ const browser = createBrowserCapture(root);
4798
5685
  try {
4799
5686
  const module = await import(pathToFileURL(resolve(root, options.program)).href);
4800
5687
  const program = module.default ?? module.run;
@@ -4802,7 +5689,8 @@ async function execCommand(options) {
4802
5689
  throw new TypeError("operation program must export a default function or named run function");
4803
5690
  }
4804
5691
  const client = await createToolClient({ projectRoot: root });
4805
- const value = await program(client);
5692
+ const context = { ...client, browser };
5693
+ const value = await program(context);
4806
5694
  if (!isSerializableValue(value)) {
4807
5695
  throw new TypeError("tool program returned a non-serializable live value");
4808
5696
  }
@@ -4817,17 +5705,22 @@ async function execCommand(options) {
4817
5705
  detail: { reason: cause instanceof Error ? cause.message : String(cause) }
4818
5706
  }
4819
5707
  };
5708
+ } finally {
5709
+ await browser.close();
4820
5710
  }
4821
5711
  }
4822
5712
 
4823
5713
  // src/cli.ts
4824
- var args = process.argv.slice(2);
5714
+ init_types();
5715
+ var rawArgs = process.argv.slice(2);
5716
+ var args = [...rawArgs];
4825
5717
  var primary = args.shift();
4826
- var nested = primary === "asset" || primary === "shader" || primary === "plugin" || primary === "skill" || primary === "sdk" ? args.shift() : void 0;
4827
- 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;
4828
5720
  var json = args.includes("--json");
4829
5721
  var dryRun = args.includes("--dry-run");
4830
5722
  var noInstall = args.includes("--no-install");
5723
+ var help = rawArgs.includes("--help") || rawArgs.includes("-h");
4831
5724
  function option(name) {
4832
5725
  const index = args.indexOf(name);
4833
5726
  return index < 0 ? void 0 : args[index + 1];
@@ -4836,6 +5729,8 @@ var optionValueIndexes = new Set(
4836
5729
  [
4837
5730
  "--artifact",
4838
5731
  "--base",
5732
+ "--backend",
5733
+ "--browser",
4839
5734
  "--digest",
4840
5735
  "--id",
4841
5736
  "--realm",
@@ -4844,8 +5739,12 @@ var optionValueIndexes = new Set(
4844
5739
  "--input",
4845
5740
  "--output",
4846
5741
  "--root",
5742
+ "--port",
4847
5743
  "--template",
5744
+ "--height",
4848
5745
  "--version",
5746
+ "--wait-ms",
5747
+ "--width",
4849
5748
  "--work-index"
4850
5749
  ].flatMap((name) => {
4851
5750
  const index = args.indexOf(name);
@@ -4865,6 +5764,11 @@ var commands = [
4865
5764
  "dev",
4866
5765
  "build",
4867
5766
  "package",
5767
+ "capture",
5768
+ "engine.status",
5769
+ "engine.use-local",
5770
+ "engine.unlink",
5771
+ "engine.doctor",
4868
5772
  "serve",
4869
5773
  "preview",
4870
5774
  "run",
@@ -4882,6 +5786,18 @@ var commands = [
4882
5786
  "describe",
4883
5787
  "exec"
4884
5788
  ];
5789
+ function projectServerOptions() {
5790
+ const parsedPort = parseProjectPortOption(option("--port"), args.includes("--port"));
5791
+ if (!parsedPort.ok) return parsedPort;
5792
+ return {
5793
+ ok: true,
5794
+ value: {
5795
+ root: projectRoot,
5796
+ json,
5797
+ ...parsedPort.value === void 0 ? {} : { port: parsedPort.value }
5798
+ }
5799
+ };
5800
+ }
4885
5801
  async function run(value) {
4886
5802
  switch (value) {
4887
5803
  case "new": {
@@ -4898,8 +5814,10 @@ async function run(value) {
4898
5814
  return doctorCommand({ root: projectRoot});
4899
5815
  case "test":
4900
5816
  return testCommand({ root: projectRoot});
4901
- case "dev":
4902
- return devCommand({ root: projectRoot, json });
5817
+ case "dev": {
5818
+ const options = projectServerOptions();
5819
+ return options.ok ? devCommand(options.value) : options;
5820
+ }
4903
5821
  case "build": {
4904
5822
  const base = option("--base");
4905
5823
  const outDir = option("--out-dir");
@@ -4917,10 +5835,100 @@ async function run(value) {
4917
5835
  ...output === void 0 ? {} : { output }
4918
5836
  });
4919
5837
  }
4920
- case "preview":
4921
- return previewCommand({ root: projectRoot, json });
4922
- case "serve":
4923
- return devCommand({ root: projectRoot, json });
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});
5924
+ case "preview": {
5925
+ const options = projectServerOptions();
5926
+ return options.ok ? previewCommand(options.value) : options;
5927
+ }
5928
+ case "serve": {
5929
+ const options = projectServerOptions();
5930
+ return options.ok ? devCommand(options.value) : options;
5931
+ }
4924
5932
  case "plugin.install": {
4925
5933
  const module = positionals[0];
4926
5934
  const id = option("--id");
@@ -5093,14 +6101,36 @@ async function run(value) {
5093
6101
  }
5094
6102
  };
5095
6103
  }
5096
- 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
+ );
5097
6127
  }
5098
6128
  }
5099
6129
  }
5100
- if (command === void 0 || !commands.includes(command)) {
5101
- process.stderr.write(
5102
- "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"
5103
- );
6130
+ if (help && primary !== "run") {
6131
+ process.stdout.write(renderForgeaxUsage());
6132
+ } else if (command === void 0 || !commands.includes(command)) {
6133
+ process.stderr.write(renderForgeaxUsage());
5104
6134
  process.exitCode = 2;
5105
6135
  } else {
5106
6136
  const stdoutWrite = process.stdout.write;
@@ -5120,9 +6150,23 @@ if (command === void 0 || !commands.includes(command)) {
5120
6150
  const envelope = result.ok ? { schemaVersion: "1.0.0", command, ok: true, value: result.value } : { schemaVersion: "1.0.0", command, ok: false, error: result.error };
5121
6151
  if (json) stdoutWrite.call(process.stdout, `${JSON.stringify(envelope)}
5122
6152
  `);
5123
- else if (result.ok) process.stdout.write(`[forgeax] ${command} ready
6153
+ else if (result.ok) {
6154
+ process.stdout.write(`[forgeax] ${command} ready
6155
+ `);
6156
+ for (const line of agentOnboardingLines(result.value)) process.stdout.write(`${line}
6157
+ `);
6158
+ for (const line of sdkUpdateLines(result.value)) process.stdout.write(`${line}
5124
6159
  `);
5125
- else process.stderr.write(`[forgeax] ${result.error.code}: ${result.error.hint}
6160
+ if (command === "sdk.install") {
6161
+ const value = result.value;
6162
+ if (typeof value.next?.cwd === "string" && Array.isArray(value.next.argv) && value.next.argv.every((part) => typeof part === "string")) {
6163
+ process.stdout.write(`[forgeax] next cwd: ${value.next.cwd}
6164
+ `);
6165
+ process.stdout.write(`[forgeax] next: ${value.next.argv.join(" ")}
6166
+ `);
6167
+ }
6168
+ }
6169
+ } else process.stderr.write(`[forgeax] ${result.error.code}: ${result.error.hint}
5126
6170
  `);
5127
6171
  if (!result.ok) process.exitCode = result.error.code === "cli-parse-error" ? 2 : 1;
5128
6172
  }