@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/index.mjs CHANGED
@@ -1,8 +1,9 @@
1
1
  import { deflateRawSync } from 'zlib';
2
- import { createHash } from 'crypto';
3
- import { readFile, writeFile, copyFile, readdir, mkdir, mkdtemp, cp, rename, rm, stat, symlink, realpath, unlink, lstat, readlink, rmdir, access } from 'fs/promises';
2
+ import { createHash, randomUUID } from 'crypto';
3
+ import { readFile, writeFile, access, readdir, stat, realpath, rm, mkdir, copyFile, mkdtemp, cp, rename, symlink, unlink, lstat, readlink, rmdir } from 'fs/promises';
4
4
  import { resolve, relative, sep, isAbsolute, extname, dirname, basename, join } from 'path';
5
- import { existsSync, readFileSync } from 'fs';
5
+ import { GameProjectSchema } from '@forgeax/engine-project';
6
+ import { existsSync } from 'fs';
6
7
  import { createRequire } from 'module';
7
8
  import { fileURLToPath } from 'url';
8
9
  import { audioImporter } from '@forgeax/engine-audio-webaudio/audio-importer';
@@ -12,21 +13,25 @@ import { gltfImporter } from '@forgeax/engine-gltf';
12
13
  import { imageImporter } from '@forgeax/engine-image/image-importer';
13
14
  import { BUILTIN_MESH_ASSETS } from '@forgeax/engine-pack/builtin';
14
15
  import { scanInventory } from '@forgeax/engine-pack/scanner';
15
- import { validatePreviewArtifactManifest, createMaterialPreviewContribution, createMeshPreviewContribution, createVfxPreviewContribution, createTexturePreviewContribution, describeResourcePreviewFailure, validateCanonicalKitReceipt } from '@forgeax/engine-preview';
16
+ import { validatePreviewArtifactManifest, createMaterialPreviewContribution, createMeshPreviewContribution, createVfxPreviewContribution, createTexturePreviewContribution, validateCanonicalKitReceipt, describeResourcePreviewFailure } from '@forgeax/engine-preview';
16
17
  import { createMaterialPackCooker } from '@forgeax/engine-shader-compiler';
17
- import { err, ok, createStandaloneRuntimeAssetBinding } from '@forgeax/engine-types';
18
+ import { createStandaloneRuntimeAssetBinding, err, ok } from '@forgeax/engine-types';
18
19
  import { createParticleCodeNativeCookerFromRoots } from '@forgeax/engine-vfx-compiler';
19
20
  import { pluginPack, reloadAssetHost } from '@forgeax/engine-vite-plugin-pack';
20
21
  import { forgeaxShader } from '@forgeax/engine-vite-plugin-shader';
21
- import { GameProjectSchema } from '@forgeax/engine-project';
22
22
  import { runCliGltf } from '@forgeax/engine-gltf/cli-gltf';
23
23
  import { scanEntries } from '@forgeax/engine-pack/cli-asset';
24
24
  import { AssetGuid } from '@forgeax/engine-pack/guid';
25
- import { execFile } from 'child_process';
25
+ import { execFile, spawn } from 'child_process';
26
26
  import { tmpdir } from 'os';
27
27
  import { promisify } from 'util';
28
- import { createRhiDebugError, decodeTape, openReplay, buildFrameModel } from '@forgeax/engine-rhi-debug';
29
- import { build, createServer, preview } from 'vite';
28
+ import { replayDeviceRequest, createRhiDebugError, decodeTape, openReplay, buildFrameModel } from '@forgeax/engine-rhi-debug';
29
+ import { rhi, createShaderModule } from '@forgeax/engine-rhi-webgpu';
30
+ import { build, preview, createServer as createServer$2 } from 'vite';
31
+ import { createServer as createServer$1 } from 'net';
32
+ import { createToolPreviewRecipe, createToolPreviewHost, FORGEAX_FRAME_SUBMITTED_DATASET } from '@forgeax/engine-app';
33
+ import { parseImage } from '@forgeax/engine-image/parse-image';
34
+ import { chromium } from 'playwright';
30
35
  import { startVitest } from 'vitest/node';
31
36
  import materialPreviewPlugin from '@forgeax/engine-preview/material';
32
37
  import meshPreviewPlugin from '@forgeax/engine-preview/mesh';
@@ -36,10 +41,7 @@ import { validateArtifactManifest, defineTool, createToolRuntime, validateRealmB
36
41
  export { createAuthenticatedLoopbackTransport, createCapabilityToken, createMigrationRecipe, createRealmCapabilityMatrix, createServiceCapability, probeMigrationTarget } from '@forgeax/engine-tool-runtime';
37
42
  import { Context, isToolPlugin } from '@forgeax/engine-plugin';
38
43
  import { installCatalogLoader, projectPluginEntries, bootstrapCatalogLoader } from '@forgeax/engine-plugin/loader';
39
- import { createServer as createServer$1 } from 'http';
40
- import { createToolPreviewRecipe, createToolPreviewHost } from '@forgeax/engine-app';
41
- import { createServer as createServer$2 } from 'net';
42
- import { chromium } from 'playwright';
44
+ import { createServer } from 'http';
43
45
 
44
46
  var __defProp = Object.defineProperty;
45
47
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -303,17 +305,549 @@ var init_dist = __esm({
303
305
  "src/dist.ts"() {
304
306
  }
305
307
  });
306
- function ignoreDevKitCatalogPath(path) {
307
- const normalized = path.replace(/\\/g, "/");
308
- if (normalized.split("/").includes("shaders")) return true;
309
- if (!normalized.endsWith(".meta.json")) return false;
308
+ function projectError(code, expected, hint, detail) {
309
+ return { ok: false, error: { code, expected, hint, detail } };
310
+ }
311
+ async function readJson(path) {
312
+ return JSON.parse(await readFile(path, "utf8"));
313
+ }
314
+ function firstUnsupportedStandaloneRealm(entries2, inheritedRealm = "engine") {
315
+ for (const entry of entries2) {
316
+ const realm = entry.realm ?? inheritedRealm ?? "engine";
317
+ if (realm !== "engine") return { id: entry.id, realm };
318
+ if (entry.group === true) {
319
+ const unsupported = firstUnsupportedStandaloneRealm(
320
+ entry.config,
321
+ realm
322
+ );
323
+ if (unsupported !== void 0) return unsupported;
324
+ }
325
+ }
326
+ return void 0;
327
+ }
328
+ function pluginModuleNames(entries2) {
329
+ return entries2.flatMap(
330
+ (entry) => entry.group === true ? pluginModuleNames(entry.config) : [entry.name]
331
+ );
332
+ }
333
+ async function readProjectFacts(rootInput = process.cwd()) {
334
+ const root = resolve(rootInput);
335
+ let forgeValue;
336
+ let packageValue;
310
337
  try {
311
- const meta = JSON.parse(readFileSync(path, "utf8"));
312
- return meta.importer === "game-default-target-profile";
338
+ [forgeValue, packageValue] = await Promise.all([
339
+ readJson(resolve(root, "forge.json")),
340
+ readJson(resolve(root, "package.json"))
341
+ ]);
342
+ } catch (cause) {
343
+ return projectError(
344
+ "project-manifest-unreadable",
345
+ "readable forge.json and package.json files",
346
+ "Run the command from a ForgeaX game root or pass its directory.",
347
+ { root, reason: cause instanceof Error ? cause.message : String(cause) }
348
+ );
349
+ }
350
+ if (forgeValue === null || typeof forgeValue !== "object") {
351
+ return projectError(
352
+ "project-manifest-invalid",
353
+ "forge.json to contain an object",
354
+ "Repair forge.json before running DevKit.",
355
+ { root }
356
+ );
357
+ }
358
+ if (packageValue === null || typeof packageValue !== "object") {
359
+ return projectError(
360
+ "package-manifest-invalid",
361
+ "package.json to contain an object",
362
+ "Repair package.json before running DevKit.",
363
+ { root }
364
+ );
365
+ }
366
+ const parsedForge = GameProjectSchema.safeParse(forgeValue);
367
+ if (!parsedForge.success) {
368
+ return projectError(
369
+ "project-manifest-invalid",
370
+ "forge.json to satisfy @forgeax/engine-project GameProjectSchema",
371
+ "Repair the fields reported by the authoritative project schema.",
372
+ { root, issues: parsedForge.error.issues }
373
+ );
374
+ }
375
+ const forge = parsedForge.data;
376
+ if (forge.id.length === 0 || forge.name.length === 0 || forge.entry === void 0 || forge.entry.length === 0) {
377
+ return projectError(
378
+ "project-manifest-invalid",
379
+ "forge.json to declare id, name, and entry",
380
+ "Restore the project entry; plugin Entries remain optional additions.",
381
+ { root }
382
+ );
383
+ }
384
+ const plugins = forge.plugins ?? [];
385
+ const unsupportedRealm = firstUnsupportedStandaloneRealm(plugins);
386
+ if (unsupportedRealm !== void 0) {
387
+ return projectError(
388
+ "project-plugin-realm-unsupported",
389
+ "the standalone Devkit host to contain only engine-realm plugin Entries",
390
+ "Move Host or build plugins to a host that owns that physical realm.",
391
+ { root, ...unsupportedRealm }
392
+ );
393
+ }
394
+ const entryPath = isAbsolute(forge.entry) ? forge.entry : resolve(root, forge.entry);
395
+ try {
396
+ await readFile(entryPath);
397
+ } catch {
398
+ return projectError(
399
+ "project-entry-missing",
400
+ "forge.json#entry to resolve to a readable module",
401
+ "Restore the game entry or update forge.json#entry.",
402
+ { root, entry: forge.entry }
403
+ );
404
+ }
405
+ const packageJson = packageValue;
406
+ const forgeax = packageJson.forgeax;
407
+ const configuredRoots = forgeax !== null && typeof forgeax === "object" ? forgeax.assets?.roots : void 0;
408
+ const assetRoots = Array.isArray(configuredRoots) && configuredRoots.every((value) => typeof value === "string") ? configuredRoots : ["assets"];
409
+ const configuredImporters = forgeax !== null && typeof forgeax === "object" ? forgeax.assets?.importers : void 0;
410
+ const assetImporters = Array.isArray(configuredImporters) && configuredImporters.every((value) => typeof value === "string") ? configuredImporters : [];
411
+ const configuredPublicDir = forgeax !== null && typeof forgeax === "object" ? forgeax.assets?.publicDir : void 0;
412
+ const assetPublicDir = typeof configuredPublicDir === "string" ? configuredPublicDir : void 0;
413
+ const physics = forge.physics === "2d" || forge.physics === "3d" ? forge.physics : void 0;
414
+ const defaultScene = typeof forge.defaultScene === "string" && forge.defaultScene.length > 0 ? forge.defaultScene : void 0;
415
+ const normalizedEntry = forge.entry.startsWith("./") ? forge.entry : `./${forge.entry}`;
416
+ const bootstrapEntry = pluginModuleNames(plugins).map((name) => name.startsWith("./") ? name : `./${name}`).includes(normalizedEntry) ? void 0 : forge.entry;
417
+ return {
418
+ ok: true,
419
+ value: {
420
+ root,
421
+ id: forge.id,
422
+ name: forge.name,
423
+ entry: forge.entry,
424
+ ...bootstrapEntry === void 0 ? {} : { bootstrapEntry },
425
+ plugins,
426
+ ...physics === void 0 ? {} : { physics },
427
+ ...defaultScene === void 0 ? {} : { defaultScene },
428
+ assetRoots,
429
+ ...assetImporters.length === 0 ? {} : { assetImporters },
430
+ ...assetPublicDir === void 0 ? {} : { assetPublicDir },
431
+ packageJson
432
+ }
433
+ };
434
+ }
435
+ function commandError(cause, fallbackCode) {
436
+ if (cause !== null && typeof cause === "object" && "code" in cause && "expected" in cause && "hint" in cause && "detail" in cause) {
437
+ return cause;
438
+ }
439
+ return {
440
+ code: fallbackCode,
441
+ expected: "the ForgeaX command to complete",
442
+ hint: "Inspect the underlying diagnostic and repair the owning input.",
443
+ detail: { reason: cause instanceof Error ? cause.message : String(cause) }
444
+ };
445
+ }
446
+ var init_project = __esm({
447
+ "src/project.ts"() {
448
+ }
449
+ });
450
+ function bindingError(code, expected, hint, detail = {}) {
451
+ return { ok: false, error: { code, expected, hint, detail } };
452
+ }
453
+ function isMissing(cause) {
454
+ return cause !== null && typeof cause === "object" && "code" in cause && cause.code === "ENOENT";
455
+ }
456
+ async function pathExists(path) {
457
+ try {
458
+ await access(path);
459
+ return true;
313
460
  } catch {
314
461
  return false;
315
462
  }
316
463
  }
464
+ function parseBinding(value, path) {
465
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
466
+ return bindingError(
467
+ "engine-binding-invalid",
468
+ "engine-binding.json to contain one object",
469
+ "Run forgeax engine unlink, then select a binding again.",
470
+ { path }
471
+ );
472
+ }
473
+ const candidate = value;
474
+ if (candidate.schemaVersion !== ENGINE_BINDING_SCHEMA_VERSION) {
475
+ return bindingError(
476
+ "engine-binding-version-unsupported",
477
+ `engine-binding.json schemaVersion ${ENGINE_BINDING_SCHEMA_VERSION}`,
478
+ "Upgrade the SDK or remove the stale binding with forgeax engine unlink.",
479
+ { path, schemaVersion: candidate.schemaVersion ?? null }
480
+ );
481
+ }
482
+ if (typeof candidate.path !== "string" || candidate.path.length === 0) {
483
+ return bindingError(
484
+ "engine-binding-path-missing",
485
+ "engine-binding.json to contain one non-empty local Engine path",
486
+ "Run forgeax engine use-local <engine-directory>.",
487
+ { path }
488
+ );
489
+ }
490
+ return {
491
+ ok: true,
492
+ value: {
493
+ schemaVersion: ENGINE_BINDING_SCHEMA_VERSION,
494
+ path: resolve(candidate.path)
495
+ }
496
+ };
497
+ }
498
+ async function readEngineBinding(rootInput = process.cwd()) {
499
+ const root = resolve(rootInput);
500
+ const path = bindingFile(root);
501
+ try {
502
+ const value = JSON.parse(await readFile(path, "utf8"));
503
+ return parseBinding(value, path);
504
+ } catch (cause) {
505
+ if (isMissing(cause)) return { ok: true, value: null };
506
+ return bindingError(
507
+ "engine-binding-unreadable",
508
+ "engine-binding.json to be readable JSON",
509
+ "Repair or remove .forgeax/engine-binding.json, then select a binding again.",
510
+ { path, reason: cause instanceof Error ? cause.message : String(cause) }
511
+ );
512
+ }
513
+ }
514
+ async function writeEngineBinding(root, binding) {
515
+ const path = bindingFile(root);
516
+ const partial = `${path}.partial-${process.pid}`;
517
+ await mkdir(dirname(path), { recursive: true });
518
+ await writeFile(partial, `${JSON.stringify(binding, null, 2)}
519
+ `, "utf8");
520
+ await rename(partial, path);
521
+ }
522
+ function conditionalExport(value) {
523
+ if (typeof value === "string") return value;
524
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
525
+ const record = value;
526
+ for (const key of ["import", "browser", "node", "default", "require", "types"]) {
527
+ const candidate = record[key];
528
+ const selected = conditionalExport(candidate);
529
+ if (selected !== void 0) return selected;
530
+ }
531
+ return void 0;
532
+ }
533
+ function packageEntry(manifest) {
534
+ const exportsValue = manifest.exports;
535
+ if (exportsValue !== void 0) {
536
+ if (typeof exportsValue === "object" && exportsValue !== null && !Array.isArray(exportsValue)) {
537
+ const root = exportsValue["."];
538
+ const selected2 = conditionalExport(root ?? exportsValue);
539
+ if (selected2 !== void 0) return selected2;
540
+ }
541
+ const selected = conditionalExport(exportsValue);
542
+ if (selected !== void 0) return selected;
543
+ }
544
+ return typeof manifest.module === "string" ? manifest.module : typeof manifest.main === "string" ? manifest.main : void 0;
545
+ }
546
+ async function readManifest(path) {
547
+ const value = JSON.parse(await readFile(path, "utf8"));
548
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
549
+ throw new Error(`engine-package-manifest-invalid: ${path}`);
550
+ }
551
+ return value;
552
+ }
553
+ async function inspectEngineWorkspace(workspaceInput) {
554
+ const root = resolve(workspaceInput);
555
+ const packageRoot = resolve(root, "packages");
556
+ try {
557
+ await access(resolve(root, "pnpm-workspace.yaml"));
558
+ const entries2 = await readdir(packageRoot, { withFileTypes: true });
559
+ const packages = [];
560
+ for (const entry of entries2) {
561
+ if (!entry.isDirectory()) continue;
562
+ const packagePath = resolve(packageRoot, entry.name);
563
+ let manifest;
564
+ try {
565
+ manifest = await readManifest(resolve(packagePath, "package.json"));
566
+ } catch {
567
+ continue;
568
+ }
569
+ if (typeof manifest.name !== "string" || !manifest.name.startsWith("@forgeax/engine"))
570
+ continue;
571
+ if (typeof manifest.version !== "string") {
572
+ return bindingError(
573
+ "engine-package-version-missing",
574
+ "every local Engine package to declare a version",
575
+ "Restore the package manifest version before selecting a local Engine.",
576
+ { package: manifest.name, root: packagePath }
577
+ );
578
+ }
579
+ const target = packageEntry(manifest);
580
+ const entryPath = target === void 0 ? void 0 : resolve(packagePath, target);
581
+ let entryDigest = null;
582
+ let builtAt2 = null;
583
+ if (entryPath !== void 0 && await pathExists(entryPath)) {
584
+ const [bytes, metadata] = await Promise.all([readFile(entryPath), stat(entryPath)]);
585
+ entryDigest = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
586
+ builtAt2 = metadata.mtime.toISOString();
587
+ }
588
+ packages.push({
589
+ name: manifest.name,
590
+ root: packagePath,
591
+ version: manifest.version,
592
+ entry: entryPath ?? null,
593
+ built: entryDigest !== null,
594
+ entryDigest,
595
+ builtAt: builtAt2
596
+ });
597
+ }
598
+ packages.sort((a, b) => a.name.localeCompare(b.name));
599
+ if (packages.length === 0) {
600
+ return bindingError(
601
+ "engine-workspace-empty",
602
+ "the local Engine workspace to contain @forgeax/engine packages",
603
+ "Pass the Engine repository or its source/engine SDK directory.",
604
+ { root, packageRoot }
605
+ );
606
+ }
607
+ const versions = [...new Set(packages.map((item) => item.version))].sort();
608
+ const missingBuilds = packages.filter((item) => !item.built).map((item) => item.name);
609
+ const builtAt = packages.flatMap((item) => item.builtAt === null ? [] : [item.builtAt]).sort().at(-1) ?? null;
610
+ const digest = `sha256:${createHash("sha256").update(
611
+ packages.map((item) => `${item.name}\0${item.version}\0${item.entryDigest ?? "unbuilt"}`).join("\n")
612
+ ).digest("hex")}`;
613
+ return {
614
+ ok: true,
615
+ value: {
616
+ root,
617
+ packageCount: packages.length,
618
+ builtPackages: packages.length - missingBuilds.length,
619
+ missingBuilds,
620
+ versions,
621
+ packages,
622
+ digest,
623
+ builtAt
624
+ }
625
+ };
626
+ } catch (cause) {
627
+ return bindingError(
628
+ isMissing(cause) ? "engine-workspace-missing" : "engine-workspace-unreadable",
629
+ "a readable Engine workspace with pnpm-workspace.yaml and packages/",
630
+ "Pass the Engine repository root, not its packages/ directory.",
631
+ { root, packageRoot, reason: cause instanceof Error ? cause.message : String(cause) }
632
+ );
633
+ }
634
+ }
635
+ function projectDependencyMode(packageJson) {
636
+ let workspace = false;
637
+ let registry = false;
638
+ for (const section of [
639
+ "dependencies",
640
+ "devDependencies",
641
+ "optionalDependencies",
642
+ "peerDependencies"
643
+ ]) {
644
+ const value = packageJson[section];
645
+ if (value === null || typeof value !== "object" || Array.isArray(value)) continue;
646
+ for (const version of Object.values(value)) {
647
+ if (typeof version !== "string") continue;
648
+ if (version.startsWith("workspace:") || version.startsWith("file:")) workspace = true;
649
+ else registry = true;
650
+ }
651
+ }
652
+ return workspace ? registry ? "mixed" : "workspace" : registry ? "registry" : "none";
653
+ }
654
+ async function sdkResolution(root) {
655
+ const packageJson = resolve(root, "node_modules", "@forgeax", "engine", "package.json");
656
+ try {
657
+ const manifest = await readManifest(packageJson);
658
+ const target = packageEntry(manifest);
659
+ const entry = target === void 0 ? null : resolve(dirname(packageJson), target);
660
+ return {
661
+ root: dirname(packageJson),
662
+ version: typeof manifest.version === "string" ? manifest.version : null,
663
+ entry,
664
+ built: entry !== null && await pathExists(entry),
665
+ source: "sdk"
666
+ };
667
+ } catch {
668
+ return { root: null, version: null, entry: null, built: false, source: "unresolved" };
669
+ }
670
+ }
671
+ async function engineStatusCommand(options = {}) {
672
+ const facts = await readProjectFacts(options.root);
673
+ if (!facts.ok) return facts;
674
+ const bindingResult = await readEngineBinding(facts.value.root);
675
+ if (!bindingResult.ok) return bindingResult;
676
+ const binding = bindingResult.value;
677
+ const mode = binding === null ? "sdk" : "local";
678
+ const projectDependencies = projectDependencyMode(facts.value.packageJson);
679
+ if (binding !== null) {
680
+ const localPath = binding.path;
681
+ const localBinding = {
682
+ schemaVersion: ENGINE_BINDING_SCHEMA_VERSION,
683
+ path: localPath
684
+ };
685
+ const workspaceResult = await inspectEngineWorkspace(localPath);
686
+ if (!workspaceResult.ok) {
687
+ const resolved3 = {
688
+ root: localPath,
689
+ version: null,
690
+ entry: null,
691
+ built: false,
692
+ source: "local"
693
+ };
694
+ return {
695
+ ok: true,
696
+ value: {
697
+ schemaVersion: ENGINE_BINDING_SCHEMA_VERSION,
698
+ root: facts.value.root,
699
+ binding: localBinding,
700
+ mode,
701
+ resolved: resolved3,
702
+ workspace: null,
703
+ projectDependencies,
704
+ npmCompatible: projectDependencies === "registry" || projectDependencies === "none",
705
+ healthy: false,
706
+ next: ["forgeax engine unlink", "forgeax engine use-local <engine-directory>"],
707
+ diagnostic: workspaceResult.error
708
+ }
709
+ };
710
+ }
711
+ const workspace = workspaceResult.value;
712
+ const version = workspace.versions.length === 1 ? workspace.versions[0] ?? null : null;
713
+ const umbrella = workspace.packages.find((item) => item.name === "@forgeax/engine");
714
+ const resolved2 = {
715
+ root: localPath,
716
+ version,
717
+ entry: umbrella?.entry ?? null,
718
+ built: umbrella?.built ?? false,
719
+ source: "local"
720
+ };
721
+ const healthy = workspace.versions.length === 1 && workspace.missingBuilds.length === 0;
722
+ return {
723
+ ok: true,
724
+ value: {
725
+ schemaVersion: ENGINE_BINDING_SCHEMA_VERSION,
726
+ root: facts.value.root,
727
+ binding: localBinding,
728
+ mode,
729
+ resolved: resolved2,
730
+ workspace,
731
+ projectDependencies,
732
+ npmCompatible: projectDependencies === "registry" || projectDependencies === "none",
733
+ healthy,
734
+ next: healthy ? ["forgeax build", "forgeax capture --backend auto --require-ui"] : ["pnpm build:engine", "forgeax engine doctor"]
735
+ }
736
+ };
737
+ }
738
+ const resolved = await sdkResolution(facts.value.root);
739
+ return {
740
+ ok: true,
741
+ value: {
742
+ schemaVersion: ENGINE_BINDING_SCHEMA_VERSION,
743
+ root: facts.value.root,
744
+ binding,
745
+ mode,
746
+ resolved,
747
+ workspace: null,
748
+ projectDependencies,
749
+ npmCompatible: projectDependencies === "registry" || projectDependencies === "none",
750
+ healthy: resolved.source === "sdk" && resolved.built,
751
+ next: resolved.source === "sdk" && resolved.built ? ["forgeax build", "forgeax capture --backend auto --require-ui"] : ["pnpm install", "forgeax doctor"]
752
+ }
753
+ };
754
+ }
755
+ async function engineUseLocalCommand(options) {
756
+ const root = resolve(options.root ?? process.cwd());
757
+ if (options.path === void 0 || options.path.trim().length === 0) {
758
+ return bindingError(
759
+ "engine-binding-path-missing",
760
+ "forgeax engine use-local <engine-directory>",
761
+ "Pass the local Engine repository or SDK source directory."
762
+ );
763
+ }
764
+ let localPath;
765
+ try {
766
+ localPath = await realpath(resolve(root, options.path));
767
+ } catch (cause) {
768
+ return bindingError(
769
+ "engine-workspace-missing",
770
+ "the local Engine directory to exist",
771
+ "Pass an existing Engine repository or SDK source directory.",
772
+ {
773
+ path: resolve(root, options.path),
774
+ reason: cause instanceof Error ? cause.message : String(cause)
775
+ }
776
+ );
777
+ }
778
+ const workspace = await inspectEngineWorkspace(localPath);
779
+ if (!workspace.ok) return workspace;
780
+ const binding = {
781
+ schemaVersion: ENGINE_BINDING_SCHEMA_VERSION,
782
+ path: localPath
783
+ };
784
+ if (options.dryRun !== true) await writeEngineBinding(root, binding);
785
+ const status = await engineStatusCommand({ root });
786
+ if (!status.ok) return status;
787
+ return {
788
+ ok: true,
789
+ value: {
790
+ ...status.value,
791
+ next: workspace.value.missingBuilds.length === 0 ? ["forgeax build", "forgeax capture --backend auto --require-ui"] : ["pnpm build:engine", "forgeax engine doctor"]
792
+ }
793
+ };
794
+ }
795
+ async function engineUnlinkCommand(options = {}) {
796
+ const root = resolve(options.root ?? process.cwd());
797
+ if (options.dryRun !== true) await rm(bindingFile(root), { force: true });
798
+ return engineStatusCommand({ root });
799
+ }
800
+ async function engineDoctorCommand(options = {}) {
801
+ const status = await engineStatusCommand(options);
802
+ if (!status.ok) return status;
803
+ if (status.value.projectDependencies === "workspace" || status.value.projectDependencies === "mixed") {
804
+ return {
805
+ ok: false,
806
+ error: {
807
+ code: "engine-project-workspace-dependency",
808
+ expected: "the game project to use registry or SDK-resolved dependencies for npm consumers",
809
+ hint: "This project is a pnpm workspace; use pnpm here, or create a clean SDK game project for npm install.",
810
+ detail: { root: status.value.root, projectDependencies: status.value.projectDependencies }
811
+ }
812
+ };
813
+ }
814
+ if (status.value.diagnostic !== void 0) return { ok: false, error: status.value.diagnostic };
815
+ if (!status.value.healthy) {
816
+ return {
817
+ ok: false,
818
+ error: {
819
+ code: status.value.mode === "local" ? "engine-local-build-missing" : "engine-sdk-unresolved",
820
+ expected: "the selected Engine binding to resolve to built packages",
821
+ 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.",
822
+ detail: { status: status.value }
823
+ }
824
+ };
825
+ }
826
+ return status;
827
+ }
828
+ function engineBindingFilePath(rootInput = process.cwd()) {
829
+ return bindingFile(resolve(rootInput));
830
+ }
831
+ var ENGINE_BINDING_SCHEMA_VERSION, bindingFile;
832
+ var init_engine_binding = __esm({
833
+ "src/engine-binding.ts"() {
834
+ init_project();
835
+ ENGINE_BINDING_SCHEMA_VERSION = "1.0.0";
836
+ bindingFile = (root) => resolve(root, ".forgeax", "engine-binding.json");
837
+ }
838
+ });
839
+
840
+ // src/host.ts
841
+ var host_exports = {};
842
+ __export(host_exports, {
843
+ createViteConfig: () => createViteConfig,
844
+ devKitDdcRoots: () => devKitDdcRoots,
845
+ ignoreDevKitCatalogPath: () => ignoreDevKitCatalogPath
846
+ });
847
+ function ignoreDevKitCatalogPath(path) {
848
+ const normalized = path.replace(/\\/g, "/");
849
+ return normalized.split("/").includes("shaders");
850
+ }
317
851
  function devKitDdcRoots(projectRoot) {
318
852
  return {
319
853
  buildCacheRoot: resolve(projectRoot, ".forgeax", "ddc", "build-cache"),
@@ -371,40 +905,33 @@ function findEngineWorkspaceRoot() {
371
905
  cursor = parent;
372
906
  }
373
907
  }
374
- async function engineWorkspacePackages() {
375
- if (engineWorkspacePackagesPromise !== void 0) return engineWorkspacePackagesPromise;
376
- engineWorkspacePackagesPromise = (async () => {
377
- const workspaceRoot = findEngineWorkspaceRoot();
378
- if (workspaceRoot === void 0) return /* @__PURE__ */ new Map();
379
- const packageRoot = resolve(workspaceRoot, "packages");
380
- if (!existsSync(packageRoot)) return /* @__PURE__ */ new Map();
381
- const packages = /* @__PURE__ */ new Map();
382
- for (const entry of await readdir(packageRoot, { withFileTypes: true })) {
383
- if (!entry.isDirectory()) continue;
384
- const root = resolve(packageRoot, entry.name);
385
- try {
386
- const manifest = JSON.parse(
387
- await readFile(resolve(root, "package.json"), "utf8")
388
- );
389
- if (manifest === null || typeof manifest !== "object") continue;
390
- const name = manifest.name;
391
- if (typeof name === "string" && name.startsWith("@forgeax/engine-")) {
392
- packages.set(name, { root, manifest });
393
- }
394
- } catch {
908
+ async function engineWorkspacePackages(workspaceRoot = findEngineWorkspaceRoot()) {
909
+ if (workspaceRoot === void 0) return /* @__PURE__ */ new Map();
910
+ const packageRoot = resolve(workspaceRoot, "packages");
911
+ if (!existsSync(packageRoot)) return /* @__PURE__ */ new Map();
912
+ const packages = /* @__PURE__ */ new Map();
913
+ for (const entry of await readdir(packageRoot, { withFileTypes: true })) {
914
+ if (!entry.isDirectory()) continue;
915
+ const root = resolve(packageRoot, entry.name);
916
+ try {
917
+ const manifest = JSON.parse(await readFile(resolve(root, "package.json"), "utf8"));
918
+ if (manifest === null || typeof manifest !== "object") continue;
919
+ const name = manifest.name;
920
+ if (typeof name === "string" && name.startsWith("@forgeax/engine")) {
921
+ packages.set(name, { root, manifest });
395
922
  }
923
+ } catch {
396
924
  }
397
- return packages;
398
- })();
399
- return engineWorkspacePackagesPromise;
925
+ }
926
+ return packages;
400
927
  }
401
- function conditionalExport(value) {
928
+ function conditionalExport2(value) {
402
929
  if (typeof value === "string") return value;
403
930
  if (value === null || Array.isArray(value)) return void 0;
404
931
  for (const condition of ["browser", "import", "node", "default"]) {
405
932
  const candidate = value[condition];
406
933
  if (candidate === void 0) continue;
407
- const selected = conditionalExport(candidate);
934
+ const selected = conditionalExport2(candidate);
408
935
  if (selected !== void 0) return selected;
409
936
  }
410
937
  return void 0;
@@ -417,14 +944,14 @@ function packageExportTarget(manifest, subpath) {
417
944
  return typeof main === "string" ? main : void 0;
418
945
  }
419
946
  if (typeof exportsValue === "string" || exportsValue === null) {
420
- return subpath.length === 0 ? conditionalExport(exportsValue) : void 0;
947
+ return subpath.length === 0 ? conditionalExport2(exportsValue) : void 0;
421
948
  }
422
949
  const keys = Object.keys(exportsValue);
423
950
  const subpathMap = keys.some((key) => key === "." || key.startsWith("./"));
424
- if (!subpathMap) return subpath.length === 0 ? conditionalExport(exportsValue) : void 0;
951
+ if (!subpathMap) return subpath.length === 0 ? conditionalExport2(exportsValue) : void 0;
425
952
  const requested = subpath.length === 0 ? "." : `./${subpath}`;
426
953
  const exact = exportsValue[requested];
427
- if (exact !== void 0) return conditionalExport(exact);
954
+ if (exact !== void 0) return conditionalExport2(exact);
428
955
  for (const key of keys) {
429
956
  const marker = key.indexOf("*");
430
957
  if (marker < 0) continue;
@@ -434,14 +961,14 @@ function packageExportTarget(manifest, subpath) {
434
961
  const replacement = requested.slice(prefix.length, requested.length - suffix.length);
435
962
  const exportTarget = exportsValue[key];
436
963
  if (exportTarget === void 0) continue;
437
- const selected = conditionalExport(exportTarget);
964
+ const selected = conditionalExport2(exportTarget);
438
965
  return selected?.replaceAll("*", replacement);
439
966
  }
440
967
  return void 0;
441
968
  }
442
969
  function engineWorkspaceImport(source, packages) {
443
- if (!source.startsWith("@forgeax/engine-")) return void 0;
444
- const separator = source.indexOf("/", "@forgeax/engine-".length);
970
+ if (!source.startsWith("@forgeax/engine")) return void 0;
971
+ const separator = source.indexOf("/", "@forgeax/engine".length);
445
972
  const packageName = separator < 0 ? source : source.slice(0, separator);
446
973
  const subpath = separator < 0 ? "" : source.slice(separator + 1);
447
974
  const packageInfo = packages.get(packageName);
@@ -455,14 +982,25 @@ function engineWorkspaceImport(source, packages) {
455
982
  }
456
983
  return absolute;
457
984
  }
458
- async function createEngineWorkspaceResolver() {
459
- const packages = await engineWorkspacePackages();
985
+ async function createEngineWorkspaceResolver(projectRoot) {
986
+ const binding = await readEngineBinding(projectRoot);
987
+ if (!binding.ok) {
988
+ throw new Error(`${binding.error.code}: ${binding.error.hint}`);
989
+ }
990
+ const localRoot = binding.value?.path;
991
+ if (localRoot !== void 0) {
992
+ const inspected = await inspectEngineWorkspace(localRoot);
993
+ if (!inspected.ok) throw new Error(`${inspected.error.code}: ${inspected.error.hint}`);
994
+ }
995
+ const packages = await engineWorkspacePackages(localRoot);
460
996
  if (packages.size === 0) return void 0;
461
997
  return {
462
998
  name: "forgeax:devkit-engine-workspace-resolver",
999
+ enforce: "pre",
463
1000
  async resolveId(source, importer) {
464
1001
  const bareSource = source.split("?", 1)[0] ?? source;
465
- if (!bareSource.startsWith("@forgeax/engine-")) return null;
1002
+ if (!bareSource.startsWith("@forgeax/engine")) return null;
1003
+ if (localRoot !== void 0) return engineWorkspaceImport(bareSource, packages);
466
1004
  try {
467
1005
  const resolved = await this.resolve(source, importer, { skipSelf: true });
468
1006
  if (resolved !== null) return resolved;
@@ -578,17 +1116,20 @@ await app.pluginContext.plugin({
578
1116
  return `import { forgeaxBundlerAdapter } from 'virtual:forgeax/bundler';
579
1117
  import { createApp, createToolPreviewHost, createToolPreviewRecipe, fitToolPreviewCameraToAabb, gameHostPlugin, replayToolPreviewCapture } from '@forgeax/engine/app';
580
1118
  import { createCatalogSource } from '@forgeax/engine/assets-runtime';
1119
+ import {
1120
+ createRuntimeAssetImportTransport,
1121
+ runtimeBinding,
1122
+ } from 'virtual:forgeax/pack-runtime';
581
1123
  import { installCatalogLoader, projectPluginEntries } from '@forgeax/engine/plugin/loader';
582
1124
  import { audioPlugin } from '@forgeax/engine/audio';
583
1125
  import { webAudioPlugin } from '@forgeax/engine/audio-webaudio';
584
1126
  import { physicsPlugin } from '@forgeax/engine/physics';
585
1127
  import { skinningPlugin } from '@forgeax/engine/skinning';
586
1128
  import { createPrimitiveMesh } from '@forgeax/engine/geometry';
587
- import { createDevImportTransport } from '@forgeax/engine/runtime';
588
1129
  import { mat4 } from '@forgeax/engine/math';
589
1130
  import { CAMERA_PROJECTION_ORTHOGRAPHIC, Camera, DirectionalLight, Materials, MeshFilter, MeshRenderer, Skylight, SkyboxBackground, TONEMAP_NONE, TONEMAP_REINHARD_EXTENDED } from '@forgeax/engine/render';
590
- import { projectSceneAsset, Transform, worldInstantiateScene } from '@forgeax/engine/scene';
591
- import { createStandaloneRuntimeAssetBinding } from '@forgeax/engine/types';
1131
+ import { Transform } from '@forgeax/engine/scene';
1132
+ import { type SceneAsset } from '@forgeax/engine/types';
592
1133
  import { ParticleEffectPlayer, vfxGpuEffectContribution } from '@forgeax/engine/vfx';
593
1134
  import { createVfxRuntimeHost } from '@forgeax/engine/vfx-render';
594
1135
 
@@ -689,17 +1230,43 @@ const resizeCanvas = () => {
689
1230
  const resizeObserver = new ResizeObserver(resizeCanvas);
690
1231
  resizeObserver.observe(canvas);
691
1232
  resizeCanvas();
692
- const runtimeScopeBinding = createStandaloneRuntimeAssetBinding(${JSON.stringify(facts.id)});
1233
+ const runtimeScopeBinding = runtimeBinding;
1234
+ if (runtimeScopeBinding === undefined) {
1235
+ throw new Error('forgeax: Vite Pack runtime binding is required in the generated host');
1236
+ }
693
1237
  const assetCatalog = createCatalogSource({
694
1238
  url: import.meta.env.DEV
695
1239
  ? runtimeScopeBinding.catalogUrl
696
1240
  : new URL('pack-index.json', document.baseURI).href,
697
- expectedScope: runtimeScopeBinding,
1241
+ ...(import.meta.env.DEV ? { expectedScope: runtimeScopeBinding } : {}),
698
1242
  });
699
1243
  const bundler = {
700
1244
  ...forgeaxBundlerAdapter(),
701
- ...(import.meta.env.DEV ? { importTransport: createDevImportTransport(runtimeScopeBinding) } : {}),
1245
+ ...(import.meta.env.DEV
1246
+ ? { importTransport: createRuntimeAssetImportTransport(runtimeScopeBinding) }
1247
+ : {}),
702
1248
  };
1249
+ const assetPreparation = new WeakMap();
1250
+
1251
+ function prepareAssetRegistry(assets) {
1252
+ const existing = assetPreparation.get(assets);
1253
+ if (existing !== undefined) return existing;
1254
+ const pending = (async () => {
1255
+ if (import.meta.env.DEV) {
1256
+ assets.configureRuntimeBinding(runtimeScopeBinding);
1257
+ } else {
1258
+ assets.configurePackIndex(new URL('pack-index.json', document.baseURI).href);
1259
+ }
1260
+ assets.setCatalogSource(assetCatalog);
1261
+ if (vfxRuntimeHost !== undefined) {
1262
+ assets.installDecoder(vfxGpuEffectContribution.kind, vfxGpuEffectContribution.decoder);
1263
+ }
1264
+ const catalog = await assets.enumerateCatalog();
1265
+ if (!catalog.ok) throw catalog.error;
1266
+ })();
1267
+ assetPreparation.set(assets, pending);
1268
+ return pending;
1269
+ }
703
1270
 
704
1271
  function previewStable(value) {
705
1272
  if (value instanceof ArrayBuffer) return 'ArrayBuffer:' + JSON.stringify(Array.from(new Uint8Array(value)));
@@ -760,13 +1327,14 @@ async function previewOwnerFacts(assets, resource, payload) {
760
1327
  }
761
1328
 
762
1329
  async function prepareProject(app) {
1330
+ await prepareAssetRegistry(app.assets);
763
1331
  previewWorld = app.world;
764
1332
  const assets = app.assets;
765
1333
  let canonicalEnvironment;
766
1334
  if (resource !== undefined) {
767
1335
  if (resource.kind !== 'texture') {
768
1336
  if (canonicalEnvironmentGuid === null) throw new Error('canonical preview environment is unavailable');
769
- const environment = await assets.load(canonicalEnvironmentGuid, 'equirect');
1337
+ const environment = await assets.loadByGuid(assets.parseGuid(canonicalEnvironmentGuid));
770
1338
  if (!environment.ok) throw environment.error;
771
1339
  if (environment.value.kind !== 'equirect') {
772
1340
  throw new Error(\`canonical preview environment expected equirect, received \${environment.value.kind}\`);
@@ -781,7 +1349,7 @@ if (resource !== undefined) {
781
1349
  const attached = await vfxRuntimeHost.attachWorld({ world: app.world, assets });
782
1350
  if (!attached.ok) throw attached.error;
783
1351
  }
784
- const loaded = await assets.load(resource.guid, resource.kind === 'vfx' ? 'particle-effect' : resource.kind);
1352
+ const loaded = await assets.loadByGuid(assets.parseGuid(resource.guid));
785
1353
  if (!loaded.ok) throw loaded.error;
786
1354
  const payload = loaded.value;
787
1355
  if (resource.kind === 'vfx' && payload.kind !== 'particle-effect') {
@@ -794,7 +1362,7 @@ if (resource !== undefined) {
794
1362
  const meshMaterialHandles = resource.kind === 'mesh' && payload.kind === 'mesh'
795
1363
  ? await Promise.all(payload.materialSlots.map(async (slot) => {
796
1364
  if (slot.defaultMaterial === undefined) return 0;
797
- const material = await assets.load(slot.defaultMaterial, 'material');
1365
+ const material = await assets.loadByGuid(assets.parseGuid(slot.defaultMaterial));
798
1366
  if (!material.ok) throw material.error;
799
1367
  if (material.value.kind !== 'material') {
800
1368
  throw new Error('mesh material slot resolved to a non-material asset');
@@ -924,15 +1492,13 @@ let defaultScene;
924
1492
  let defaultSceneRoot;
925
1493
  const defaultSceneGuid = ${JSON.stringify(facts.defaultScene)};
926
1494
  if (defaultSceneGuid !== undefined) {
927
- const loaded = await assets.load(defaultSceneGuid, 'scene');
1495
+ const loaded = await assets.loadByGuid<SceneAsset>(assets.parseGuid(defaultSceneGuid));
928
1496
  if (!loaded.ok) throw loaded.error;
929
- const projected = await projectSceneAsset(app.world, loaded.value, (guid, kind) => assets.load(guid, kind));
930
- if (!projected.ok) throw projected.error;
931
- defaultScene = projected.value;
932
- const handle = app.world.allocSharedRef('SceneAsset', projected.value);
933
- const instantiated = worldInstantiateScene(app.world, handle);
1497
+ defaultScene = loaded.value;
1498
+ const handle = app.world.allocSharedRef('SceneAsset', loaded.value);
1499
+ const instantiated = assets.instantiate<SceneAsset>(handle, app.world);
934
1500
  if (!instantiated.ok) throw instantiated.error;
935
- defaultSceneRoot = instantiated.value.root;
1501
+ defaultSceneRoot = instantiated.value;
936
1502
  }
937
1503
  const uiRoot = document.querySelector('#game-ui');
938
1504
  await app.pluginContext.plugin(gameHostPlugin({
@@ -971,8 +1537,6 @@ if (query.has('forgeax-tool-replay')) {
971
1537
  ...(resource === undefined ? {} : { resource }),
972
1538
  canvas,
973
1539
  app: {
974
- ...(assetCatalog === undefined ? {} : { assetCatalog }),
975
- ...(vfxRuntimeHost === undefined ? {} : { assetDecoders: [vfxGpuEffectContribution] }),
976
1540
  plugins: [${plugins.join(", ")}],
977
1541
  ...(vfxRuntimeHost === undefined ? {} : { features: [vfxRuntimeHost.feature] }),
978
1542
  },
@@ -1085,8 +1649,6 @@ if (query.has('forgeax-tool-replay')) {
1085
1649
  const result = await createApp(
1086
1650
  canvas,
1087
1651
  {
1088
- ...(assetCatalog === undefined ? {} : { assetCatalog }),
1089
- ...(vfxRuntimeHost === undefined ? {} : { assetDecoders: [vfxGpuEffectContribution] }),
1090
1652
  plugins: [${plugins.join(", ")}],
1091
1653
  ...(pointerLockAllowed === undefined ? {} : { pointerLockAllowed }),
1092
1654
  },
@@ -1147,32 +1709,62 @@ function htmlSource(title) {
1147
1709
  <div id="app-shell"><canvas id="app"></canvas><div id="game-ui"></div></div><div id="forgeax-fatal" role="alert"></div>
1148
1710
  <script>
1149
1711
  (() => {
1712
+ const appendStructuredFailure = (value, prefix, depth, seen, lines) => {
1713
+ if (value === null || typeof value !== 'object' || depth > 3) return;
1714
+ if (seen.has(value)) {
1715
+ lines.push((prefix || 'cause') + ': [circular]');
1716
+ return;
1717
+ }
1718
+ seen.add(value);
1719
+ const record = value;
1720
+ const name = typeof record.name === 'string' && record.name.length > 0
1721
+ ? record.name
1722
+ : undefined;
1723
+ const code = typeof record.code === 'string' && record.code.length > 0
1724
+ ? record.code
1725
+ : undefined;
1726
+ const message = typeof record.message === 'string' && record.message.length > 0
1727
+ ? record.message
1728
+ : undefined;
1729
+ if (name !== undefined || code !== undefined || message !== undefined) {
1730
+ const identity = [name || 'Error', code].filter(Boolean).join(' ');
1731
+ lines.push((prefix ? prefix + ': ' : '') + identity + (message ? ': ' + message : ''));
1732
+ }
1733
+ for (const key of ['expected', 'hint', 'reason']) {
1734
+ if (typeof record[key] === 'string' && record[key].length > 0) {
1735
+ lines.push((prefix ? prefix + '.' : '') + key + ': ' + record[key]);
1736
+ }
1737
+ }
1738
+ for (const key of ['cause', 'detail', 'webgpuError', 'wgpuError', 'error']) {
1739
+ const nested = record[key];
1740
+ const nestedPrefix = (prefix ? prefix + '.' : '') + key;
1741
+ if (nested !== null && typeof nested === 'object') {
1742
+ appendStructuredFailure(nested, nestedPrefix, depth + 1, seen, lines);
1743
+ } else if (typeof nested === 'string' && nested.length > 0) {
1744
+ lines.push(nestedPrefix + ': ' + nested);
1745
+ }
1746
+ }
1747
+ };
1150
1748
  const formatStartupFailure = (reason) => {
1151
- if (reason instanceof Error) return reason.message;
1152
1749
  if (reason !== null && typeof reason === 'object') {
1153
- const record = reason;
1154
1750
  const lines = [];
1155
- for (const key of ['code', 'expected', 'hint']) {
1156
- if (typeof record[key] === 'string' && record[key].length > 0) {
1157
- lines.push(key + ': ' + record[key]);
1158
- }
1159
- }
1160
- const detail = record.detail;
1161
- if (detail !== null && typeof detail === 'object') {
1162
- for (const key of ['reason', 'guid']) {
1163
- if (typeof detail[key] === 'string' && detail[key].length > 0) {
1164
- lines.push('detail.' + key + ': ' + detail[key]);
1165
- }
1166
- }
1167
- }
1751
+ appendStructuredFailure(reason, '', 0, new Set(), lines);
1168
1752
  if (lines.length > 0) return lines.join('\\n');
1753
+ try {
1754
+ return JSON.stringify(reason) || 'Unknown structured startup failure';
1755
+ } catch {
1756
+ return 'Unserializable structured startup failure';
1757
+ }
1169
1758
  }
1170
1759
  return String(reason ?? 'Unknown startup failure');
1171
1760
  };
1172
1761
  const show = (reason) => {
1173
1762
  const notice = document.querySelector('#forgeax-fatal');
1174
1763
  if (!(notice instanceof HTMLElement)) return;
1175
- const message = formatStartupFailure(reason);
1764
+ let message = formatStartupFailure(reason);
1765
+ if (/webgpu|adapter-unavailable|no usable (rendering )?backend/i.test(message)) {
1766
+ 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.';
1767
+ }
1176
1768
  notice.textContent = 'ForgeaX game failed to start.\\n' + message;
1177
1769
  notice.style.display = 'grid';
1178
1770
  };
@@ -1204,7 +1796,7 @@ async function createViteConfig(facts, command, base = "/", options = {}) {
1204
1796
  ];
1205
1797
  const importers = [...projectImporters(facts)];
1206
1798
  const runtimeBinding = createStandaloneRuntimeAssetBinding(facts.id);
1207
- const engineWorkspaceResolver = await createEngineWorkspaceResolver();
1799
+ const engineWorkspaceResolver = await createEngineWorkspaceResolver(facts.root);
1208
1800
  const consumerAliases = await consumerEngineAliases(facts.root);
1209
1801
  const plugins = [
1210
1802
  ...engineWorkspaceResolver === void 0 ? [] : [engineWorkspaceResolver],
@@ -1215,198 +1807,55 @@ async function createViteConfig(facts, command, base = "/", options = {}) {
1215
1807
  ddc: devKitDdcRoots(facts.root),
1216
1808
  refresh: command === "serve" ? reloadAssetHost() : void 0,
1217
1809
  importers: [
1218
- audioImporter,
1219
- imageImporter,
1220
- fbxImporter,
1221
- gltfImporter,
1222
- fontImporter,
1223
- ...importers
1224
- ],
1225
- cookers: [createMaterialPackCooker(roots), createParticleCodeNativeCookerFromRoots(roots)],
1226
- ignorePath
1227
- })
1228
- ];
1229
- return {
1230
- root: generated,
1231
- base,
1232
- configFile: false,
1233
- publicDir: facts.assetPublicDir === void 0 ? false : resolve(facts.root, facts.assetPublicDir),
1234
- plugins,
1235
- resolve: {
1236
- alias: consumerAliases,
1237
- dedupe: ["@forgeax/engine-app", "@forgeax/engine-ecs", "@forgeax/engine-runtime"]
1238
- },
1239
- server: { fs: { allow: [facts.root, ...roots] } },
1240
- build: {
1241
- target: "esnext",
1242
- outDir: options.outDir ?? resolve(facts.root, "dist"),
1243
- emptyOutDir: true,
1244
- rollupOptions: { input: resolve(generated, "index.html") }
1245
- }
1246
- };
1247
- }
1248
- var hostRequire, engineWorkspacePackagesPromise;
1249
- var init_host = __esm({
1250
- "src/host.ts"() {
1251
- hostRequire = createRequire(import.meta.url);
1252
- }
1253
- });
1254
- function projectError(code, expected, hint, detail) {
1255
- return { ok: false, error: { code, expected, hint, detail } };
1256
- }
1257
- async function readJson(path) {
1258
- return JSON.parse(await readFile(path, "utf8"));
1259
- }
1260
- function firstUnsupportedStandaloneRealm(entries2, inheritedRealm = "engine") {
1261
- for (const entry of entries2) {
1262
- const realm = entry.realm ?? inheritedRealm ?? "engine";
1263
- if (realm === "host") return { id: entry.id, realm };
1264
- if (entry.group === true) {
1265
- const unsupported = firstUnsupportedStandaloneRealm(
1266
- entry.config,
1267
- realm
1268
- );
1269
- if (unsupported !== void 0) return unsupported;
1270
- }
1271
- }
1272
- return void 0;
1273
- }
1274
- function pluginModuleNames(entries2) {
1275
- return entries2.flatMap(
1276
- (entry) => entry.group === true ? pluginModuleNames(entry.config) : [entry.name]
1277
- );
1278
- }
1279
- async function readProjectFacts(rootInput = process.cwd()) {
1280
- const root = resolve(rootInput);
1281
- let forgeValue;
1282
- let packageValue;
1283
- try {
1284
- [forgeValue, packageValue] = await Promise.all([
1285
- readJson(resolve(root, "forge.json")),
1286
- readJson(resolve(root, "package.json"))
1287
- ]);
1288
- } catch (cause) {
1289
- return projectError(
1290
- "project-manifest-unreadable",
1291
- "readable forge.json and package.json files",
1292
- "Run the command from a ForgeaX game root or pass its directory.",
1293
- { root, reason: cause instanceof Error ? cause.message : String(cause) }
1294
- );
1295
- }
1296
- if (forgeValue === null || typeof forgeValue !== "object") {
1297
- return projectError(
1298
- "project-manifest-invalid",
1299
- "forge.json to contain an object",
1300
- "Repair forge.json before running DevKit.",
1301
- { root }
1302
- );
1303
- }
1304
- if (packageValue === null || typeof packageValue !== "object") {
1305
- return projectError(
1306
- "package-manifest-invalid",
1307
- "package.json to contain an object",
1308
- "Repair package.json before running DevKit.",
1309
- { root }
1310
- );
1311
- }
1312
- const parsedForge = GameProjectSchema.safeParse(forgeValue);
1313
- if (!parsedForge.success) {
1314
- return projectError(
1315
- "project-manifest-invalid",
1316
- "forge.json to satisfy @forgeax/engine-project GameProjectSchema",
1317
- "Repair the fields reported by the authoritative project schema.",
1318
- { root, issues: parsedForge.error.issues }
1319
- );
1320
- }
1321
- const forge = parsedForge.data;
1322
- if (forge.id.length === 0 || forge.name.length === 0 || forge.entry === void 0 || forge.entry.length === 0) {
1323
- return projectError(
1324
- "project-manifest-invalid",
1325
- "forge.json to declare id, name, and entry",
1326
- "Restore the project entry; plugin Entries remain optional additions.",
1327
- { root }
1328
- );
1329
- }
1330
- const plugins = forge.plugins ?? [];
1331
- const unsupportedRealm = firstUnsupportedStandaloneRealm(plugins);
1332
- if (unsupportedRealm !== void 0) {
1333
- return projectError(
1334
- "project-plugin-realm-unsupported",
1335
- "the standalone Devkit host to contain only engine-realm plugin Entries",
1336
- "Move Host or build plugins to a host that owns that physical realm.",
1337
- { root, ...unsupportedRealm }
1338
- );
1339
- }
1340
- const entryPath = isAbsolute(forge.entry) ? forge.entry : resolve(root, forge.entry);
1341
- try {
1342
- await readFile(entryPath);
1343
- } catch {
1344
- return projectError(
1345
- "project-entry-missing",
1346
- "forge.json#entry to resolve to a readable module",
1347
- "Restore the game entry or update forge.json#entry.",
1348
- { root, entry: forge.entry }
1349
- );
1350
- }
1351
- const packageJson = packageValue;
1352
- const forgeax = packageJson.forgeax;
1353
- const configuredRoots = forgeax !== null && typeof forgeax === "object" ? forgeax.assets?.roots : void 0;
1354
- const assetRoots = Array.isArray(configuredRoots) && configuredRoots.every((value) => typeof value === "string") ? configuredRoots : ["assets"];
1355
- const configuredImporters = forgeax !== null && typeof forgeax === "object" ? forgeax.assets?.importers : void 0;
1356
- const assetImporters = Array.isArray(configuredImporters) && configuredImporters.every((value) => typeof value === "string") ? configuredImporters : [];
1357
- const configuredPublicDir = forgeax !== null && typeof forgeax === "object" ? forgeax.assets?.publicDir : void 0;
1358
- const assetPublicDir = typeof configuredPublicDir === "string" ? configuredPublicDir : void 0;
1359
- const physics = forge.physics === "2d" || forge.physics === "3d" ? forge.physics : void 0;
1360
- const defaultScene = typeof forge.defaultScene === "string" && forge.defaultScene.length > 0 ? forge.defaultScene : void 0;
1361
- const normalizedEntry = forge.entry.startsWith("./") ? forge.entry : `./${forge.entry}`;
1362
- const bootstrapEntry = pluginModuleNames(plugins).map((name) => name.startsWith("./") ? name : `./${name}`).includes(normalizedEntry) ? void 0 : forge.entry;
1363
- return {
1364
- ok: true,
1365
- value: {
1366
- root,
1367
- id: forge.id,
1368
- name: forge.name,
1369
- entry: forge.entry,
1370
- ...bootstrapEntry === void 0 ? {} : { bootstrapEntry },
1371
- plugins,
1372
- ...physics === void 0 ? {} : { physics },
1373
- ...defaultScene === void 0 ? {} : { defaultScene },
1374
- assetRoots,
1375
- ...assetImporters.length === 0 ? {} : { assetImporters },
1376
- ...assetPublicDir === void 0 ? {} : { assetPublicDir },
1377
- packageJson
1810
+ audioImporter,
1811
+ imageImporter,
1812
+ fbxImporter,
1813
+ gltfImporter,
1814
+ fontImporter,
1815
+ ...importers
1816
+ ],
1817
+ cookers: [createMaterialPackCooker(roots), createParticleCodeNativeCookerFromRoots(roots)],
1818
+ ignorePath
1819
+ })
1820
+ ];
1821
+ return {
1822
+ root: generated,
1823
+ base,
1824
+ configFile: false,
1825
+ publicDir: facts.assetPublicDir === void 0 ? false : resolve(facts.root, facts.assetPublicDir),
1826
+ plugins,
1827
+ resolve: {
1828
+ alias: consumerAliases,
1829
+ dedupe: ["@forgeax/engine"]
1830
+ },
1831
+ server: { ...options.server, fs: { allow: [facts.root, ...roots] } },
1832
+ build: {
1833
+ target: "esnext",
1834
+ outDir: options.outDir ?? resolve(facts.root, "dist"),
1835
+ emptyOutDir: true,
1836
+ rollupOptions: { input: resolve(generated, "index.html") }
1378
1837
  }
1379
1838
  };
1380
1839
  }
1381
- function isCommandError(value) {
1382
- if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
1383
- const candidate = value;
1384
- return typeof candidate.code === "string" && typeof candidate.expected === "string" && typeof candidate.hint === "string" && candidate.detail !== null && typeof candidate.detail === "object" && !Array.isArray(candidate.detail);
1385
- }
1386
- function commandError(cause, fallbackCode) {
1387
- if (isCommandError(cause)) return cause;
1388
- return {
1389
- code: fallbackCode,
1390
- expected: "the ForgeaX command to complete",
1391
- hint: "Inspect the underlying diagnostic and repair the owning input.",
1392
- detail: { reason: cause instanceof Error ? cause.message : String(cause) }
1393
- };
1840
+ var hostRequire;
1841
+ var init_host = __esm({
1842
+ "src/host.ts"() {
1843
+ init_engine_binding();
1844
+ hostRequire = createRequire(import.meta.url);
1845
+ }
1846
+ });
1847
+
1848
+ // src/types.ts
1849
+ function resolveProjectPort(port) {
1850
+ return { port: port ?? 5173, strictPort: port !== 0 };
1394
1851
  }
1395
- var init_project = __esm({
1396
- "src/project.ts"() {
1852
+ var init_types = __esm({
1853
+ "src/types.ts"() {
1397
1854
  }
1398
1855
  });
1399
1856
  function failure(error) {
1400
1857
  return { ok: false, error };
1401
1858
  }
1402
- function producerFailure(stderr, fallback, detail) {
1403
- try {
1404
- const parsed = JSON.parse(stderr.at(-1) ?? "");
1405
- if (isCommandError(parsed)) return failure(parsed);
1406
- } catch {
1407
- }
1408
- return failure({ ...fallback, detail: { ...detail, diagnostic: stderr.join("\n") } });
1409
- }
1410
1859
  async function sourcesAt(path) {
1411
1860
  const info = await stat(path);
1412
1861
  if (info.isFile()) return [path];
@@ -1490,15 +1939,16 @@ async function addGltf(sourcePath, dryRun) {
1490
1939
  stderrWrite: (line) => stderr.push(line)
1491
1940
  });
1492
1941
  if (exitCode !== 0) {
1493
- return producerFailure(
1494
- stderr,
1495
- {
1942
+ try {
1943
+ return failure(JSON.parse(stderr.at(-1) ?? ""));
1944
+ } catch {
1945
+ return failure({
1496
1946
  code: "asset-add-failed",
1497
1947
  expected: "the glTF producer to create or reuse a valid sidecar",
1498
- hint: "Inspect the glTF source and its external references."
1499
- },
1500
- { source: sourcePath }
1501
- );
1948
+ hint: "Inspect the glTF source and its external references.",
1949
+ detail: { source: sourcePath, diagnostic: stderr.join("\n") }
1950
+ });
1951
+ }
1502
1952
  }
1503
1953
  return { ok: true, value: { source: sourcePath, metaPath: `${sourcePath}.meta.json` } };
1504
1954
  }
@@ -1541,15 +1991,16 @@ async function entries(options) {
1541
1991
  { stdoutWrite: (line) => stdout.push(line), stderrWrite: (line) => stderr.push(line) }
1542
1992
  );
1543
1993
  if (!result.ok) {
1544
- return producerFailure(
1545
- stderr,
1546
- {
1994
+ try {
1995
+ return failure(JSON.parse(stderr.at(-1) ?? ""));
1996
+ } catch {
1997
+ return failure({
1547
1998
  code: "asset-authority-invalid",
1548
1999
  expected: "all asset roots and sidecars to pass the pack scanner",
1549
- hint: "Repair the first invalid asset authority reported by the scanner."
1550
- },
1551
- {}
1552
- );
2000
+ hint: "Repair the first invalid asset authority reported by the scanner.",
2001
+ detail: { diagnostic: stderr.join("\n") }
2002
+ });
2003
+ }
1553
2004
  }
1554
2005
  return { ok: true, value: { facts: facts.value, entries: result.value } };
1555
2006
  }
@@ -1634,7 +2085,6 @@ var init_package = __esm({
1634
2085
  "@forgeax/engine-audio-webaudio": "workspace:*",
1635
2086
  "@forgeax/engine-fbx": "workspace:*",
1636
2087
  "@forgeax/engine-font": "workspace:*",
1637
- "@forgeax/engine-geometry": "workspace:*",
1638
2088
  "@forgeax/engine-gltf": "workspace:*",
1639
2089
  "@forgeax/engine-image": "workspace:*",
1640
2090
  "@forgeax/engine-input": "workspace:*",
@@ -1644,6 +2094,8 @@ var init_package = __esm({
1644
2094
  "@forgeax/engine-preview": "workspace:*",
1645
2095
  "@forgeax/engine-project": "workspace:*",
1646
2096
  "@forgeax/engine-rhi-debug": "workspace:*",
2097
+ "@forgeax/engine-rhi-null": "workspace:*",
2098
+ "@forgeax/engine-rhi-webgpu": "workspace:*",
1647
2099
  "@forgeax/engine-render": "workspace:*",
1648
2100
  "@forgeax/engine-render-graph": "workspace:*",
1649
2101
  "@forgeax/engine-runtime": "workspace:*",
@@ -1658,10 +2110,10 @@ var init_package = __esm({
1658
2110
  jiti: "1.21.7",
1659
2111
  playwright: "1.60.0",
1660
2112
  vite: "8.0.10",
1661
- vitest: "4.1.5"
2113
+ vitest: "4.0.18",
2114
+ webgpu: "^0.4.0"
1662
2115
  },
1663
2116
  devDependencies: {
1664
- "@forgeax/engine-rhi-null": "workspace:*",
1665
2117
  "@types/node": "^20.14.0"
1666
2118
  },
1667
2119
  forgeax: {
@@ -1833,7 +2285,7 @@ var init_init = __esm({
1833
2285
  "@webgpu/types": "0.1.71",
1834
2286
  tsx: "4.23.1",
1835
2287
  typescript: "6.0.3",
1836
- vitest: "4.1.5"
2288
+ vitest: "4.0.18"
1837
2289
  };
1838
2290
  }
1839
2291
  });
@@ -1875,6 +2327,20 @@ var init_sdk = __esm({
1875
2327
  "src/sdk.ts"() {
1876
2328
  }
1877
2329
  });
2330
+ function agentOnboarding(sdk, projectRoot) {
2331
+ const root = projectRoot ?? sdk.root;
2332
+ return {
2333
+ read: [
2334
+ resolve(root, "AGENTS.md"),
2335
+ resolve(root, "skills", "forgeax-engine-sdk", "SKILL.md"),
2336
+ resolve(root, "skills", "forgeax-engine-sdk", "references", "feature-catalog.md")
2337
+ ],
2338
+ next: projectRoot === void 0 ? {
2339
+ cwd: root,
2340
+ argv: ["node", "./bin/forgeax.mjs", "new", "../my-game"]
2341
+ } : { cwd: root, argv: ["pnpm", "exec", "forgeax", "list", "--json"] }
2342
+ };
2343
+ }
1878
2344
  function sdkInitPath(sdk) {
1879
2345
  return resolve(sdk.root, ...SDK_INIT_PATH);
1880
2346
  }
@@ -1886,19 +2352,21 @@ function sdkProjectInstallArgs(store) {
1886
2352
  "install",
1887
2353
  "--frozen-lockfile",
1888
2354
  "--ignore-scripts",
2355
+ "--config.pm-on-fail=ignore",
1889
2356
  "--side-effects-cache=true",
1890
2357
  "--child-concurrency=1"
1891
2358
  ];
1892
- return store === void 0 ? common : [...common, "--offline", "--trust-lockfile", "--store-dir", store];
2359
+ return store === void 0 ? common : [...common, "--offline", "--config.trust-lockfile=true", "--store-dir", store];
1893
2360
  }
1894
2361
  function sdkBootstrapInstallArgs(store) {
1895
2362
  const common = [
1896
2363
  "install",
1897
2364
  "--frozen-lockfile",
2365
+ "--config.pm-on-fail=ignore",
1898
2366
  "--child-concurrency=1",
1899
2367
  "--side-effects-cache=true"
1900
2368
  ];
1901
- return store === void 0 ? common : [...common, "--offline", "--trust-lockfile", "--store-dir", store];
2369
+ return store === void 0 ? common : [...common, "--offline", "--config.trust-lockfile=true", "--store-dir", store];
1902
2370
  }
1903
2371
  function supportedPnpm(version) {
1904
2372
  const [major = 0, minor = 0] = version.split(".").map(Number);
@@ -1988,7 +2456,8 @@ async function sdkInitCommand(sdk, options = {}) {
1988
2456
  const report = {
1989
2457
  ...state,
1990
2458
  root: sdk.root,
1991
- store: sdk.store === void 0 ? "registry" : "offline"
2459
+ store: sdk.store === void 0 ? "registry" : "offline",
2460
+ onboarding: agentOnboarding(sdk)
1992
2461
  };
1993
2462
  if (options.dryRun === true || options.install === false) return { ok: true, value: report };
1994
2463
  let staging;
@@ -2023,6 +2492,107 @@ var init_sdk_bootstrap = __esm({
2023
2492
  SDK_INIT_SCHEMA_VERSION = "1.0.0";
2024
2493
  }
2025
2494
  });
2495
+
2496
+ // src/sdk-update.ts
2497
+ function parseVersion(version) {
2498
+ const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(
2499
+ version
2500
+ );
2501
+ if (match === null) return void 0;
2502
+ const major = match[1];
2503
+ const minor = match[2];
2504
+ const patch = match[3];
2505
+ if (major === void 0 || minor === void 0 || patch === void 0) return void 0;
2506
+ const prerelease = match[4]?.split(".");
2507
+ if (prerelease?.some(
2508
+ (identifier) => identifier.length === 0 || /^\d+$/.test(identifier) && /^0\d+/.test(identifier)
2509
+ )) {
2510
+ return void 0;
2511
+ }
2512
+ return {
2513
+ core: [major, minor, patch],
2514
+ ...prerelease === void 0 ? {} : { prerelease }
2515
+ };
2516
+ }
2517
+ function compareNumericIdentifier(current, latest) {
2518
+ if (current.length !== latest.length) return latest.length > current.length ? 1 : -1;
2519
+ if (current === latest) return 0;
2520
+ return latest > current ? 1 : -1;
2521
+ }
2522
+ function comparePrerelease(current, latest) {
2523
+ const length = Math.max(current.length, latest.length);
2524
+ for (let index = 0; index < length; index += 1) {
2525
+ const currentIdentifier = current[index];
2526
+ const latestIdentifier = latest[index];
2527
+ if (currentIdentifier === void 0) return 1;
2528
+ if (latestIdentifier === void 0) return -1;
2529
+ if (currentIdentifier === latestIdentifier) continue;
2530
+ const currentNumeric = /^\d+$/.test(currentIdentifier);
2531
+ const latestNumeric = /^\d+$/.test(latestIdentifier);
2532
+ if (currentNumeric && latestNumeric) {
2533
+ return compareNumericIdentifier(currentIdentifier, latestIdentifier);
2534
+ }
2535
+ if (currentNumeric !== latestNumeric) return latestNumeric ? -1 : 1;
2536
+ return latestIdentifier > currentIdentifier ? 1 : -1;
2537
+ }
2538
+ return 0;
2539
+ }
2540
+ function newerSdkVersion(currentVersion, latestVersion) {
2541
+ const current = parseVersion(currentVersion);
2542
+ const latest = parseVersion(latestVersion);
2543
+ if (current === void 0 || latest === void 0) return false;
2544
+ for (let index = 0; index < current.core.length; index += 1) {
2545
+ const precedence = compareNumericIdentifier(
2546
+ current.core[index] ?? "0",
2547
+ latest.core[index] ?? "0"
2548
+ );
2549
+ if (precedence !== 0) return precedence > 0;
2550
+ }
2551
+ if (current.prerelease !== void 0 && latest.prerelease === void 0) return true;
2552
+ if (current.prerelease === void 0 || latest.prerelease === void 0) return false;
2553
+ return comparePrerelease(current.prerelease, latest.prerelease) > 0;
2554
+ }
2555
+ function offline() {
2556
+ return ["1", "true"].includes((process.env.npm_config_offline ?? "").toLowerCase());
2557
+ }
2558
+ async function checkSdkUpdate(currentVersion) {
2559
+ if (process.env.FORGEAX_DISABLE_UPDATE_CHECK === "1") {
2560
+ return { status: "skipped", currentVersion, reason: "disabled" };
2561
+ }
2562
+ if (offline()) return { status: "skipped", currentVersion, reason: "offline" };
2563
+ try {
2564
+ const registry = process.env.npm_config_registry ?? "https://registry.npmjs.org/";
2565
+ const response = await fetch(
2566
+ new URL("@forgeax%2Fengine-sdk", `${registry.replace(/\/$/, "")}/`),
2567
+ {
2568
+ headers: { accept: "application/vnd.npm.install-v1+json" },
2569
+ signal: AbortSignal.timeout(2e3)
2570
+ }
2571
+ );
2572
+ if (!response.ok) throw new Error(`registry-http-${response.status}`);
2573
+ const metadata = await response.json();
2574
+ const latestVersion = metadata["dist-tags"]?.latest;
2575
+ if (typeof latestVersion !== "string") throw new Error("registry-latest-version-missing");
2576
+ return newerSdkVersion(currentVersion, latestVersion) ? {
2577
+ status: "available",
2578
+ currentVersion,
2579
+ latestVersion,
2580
+ migrationRisk: UPDATE_MIGRATION_RISK
2581
+ } : { status: "current", currentVersion, latestVersion };
2582
+ } catch (cause) {
2583
+ return {
2584
+ status: "unavailable",
2585
+ currentVersion,
2586
+ reason: cause instanceof Error ? cause.message : String(cause)
2587
+ };
2588
+ }
2589
+ }
2590
+ var UPDATE_MIGRATION_RISK;
2591
+ var init_sdk_update = __esm({
2592
+ "src/sdk-update.ts"() {
2593
+ 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.";
2594
+ }
2595
+ });
2026
2596
  function slash(path) {
2027
2597
  return path.split(sep).join("/");
2028
2598
  }
@@ -2650,9 +3220,16 @@ async function newCommand(options = {}) {
2650
3220
  env: { ...process.env, CI: "true" },
2651
3221
  maxBuffer: 16 * 1024 * 1024
2652
3222
  });
3223
+ const sdkUpdate = await checkSdkUpdate(sdk.manifest.sdkVersion);
2653
3224
  return {
2654
3225
  ok: true,
2655
- value: { root, template: templateId, sdkVersion: sdk.manifest.sdkVersion }
3226
+ value: {
3227
+ root,
3228
+ template: templateId,
3229
+ sdkVersion: sdk.manifest.sdkVersion,
3230
+ onboarding: agentOnboarding(sdk, root),
3231
+ sdkUpdate
3232
+ }
2656
3233
  };
2657
3234
  } catch (cause) {
2658
3235
  if (staging !== void 0) await rm(staging, { recursive: true, force: true });
@@ -2685,6 +3262,7 @@ var init_bootstrap_commands = __esm({
2685
3262
  init_project();
2686
3263
  init_sdk();
2687
3264
  init_sdk_bootstrap();
3265
+ init_sdk_update();
2688
3266
  init_skill_install();
2689
3267
  execFileAsync2 = promisify(execFile);
2690
3268
  }
@@ -2696,7 +3274,7 @@ __export(plugin_authoring_exports, {
2696
3274
  pluginInstallCommand: () => pluginInstallCommand,
2697
3275
  pluginUninstallCommand: () => pluginUninstallCommand
2698
3276
  });
2699
- async function readManifest(root) {
3277
+ async function readManifest2(root) {
2700
3278
  const path = resolve(root, "forge.json");
2701
3279
  try {
2702
3280
  const raw = await readFile(path, "utf8");
@@ -2749,7 +3327,7 @@ async function mutateDependency(root, action, dependency) {
2749
3327
  }
2750
3328
  async function pluginInstallCommand(options) {
2751
3329
  const root = resolve(options.root ?? process.cwd());
2752
- const manifest = await readManifest(root);
3330
+ const manifest = await readManifest2(root);
2753
3331
  if (!manifest.ok) return manifest;
2754
3332
  const parsed = GameProjectSchema.safeParse(manifest.value.value);
2755
3333
  if (!parsed.success) {
@@ -2798,546 +3376,67 @@ async function pluginInstallCommand(options) {
2798
3376
  detail: { reason: cause instanceof Error ? cause.message : String(cause) }
2799
3377
  }
2800
3378
  };
2801
- }
2802
- }
2803
- async function pluginUninstallCommand(options) {
2804
- const root = resolve(options.root ?? process.cwd());
2805
- const manifest = await readManifest(root);
2806
- if (!manifest.ok) return manifest;
2807
- const parsed = GameProjectSchema.safeParse(manifest.value.value);
2808
- if (!parsed.success) {
2809
- return {
2810
- ok: false,
2811
- error: {
2812
- code: "plugin-manifest-invalid",
2813
- expected: "forge.json to satisfy GameProjectSchema",
2814
- hint: "Repair the manifest before uninstalling a plugin.",
2815
- detail: { issues: parsed.error.issues }
2816
- }
2817
- };
2818
- }
2819
- const entries2 = parsed.data.plugins ?? [];
2820
- if (!allEntries(entries2).some((entry) => entry.id === options.id)) {
2821
- return {
2822
- ok: false,
2823
- error: {
2824
- code: "plugin-entry-missing",
2825
- expected: "the plugin Entry id to exist",
2826
- hint: "Inspect forge.json#plugins and pass an installed Entry id.",
2827
- detail: { id: options.id }
2828
- }
2829
- };
2830
- }
2831
- const next = { ...parsed.data, plugins: withoutEntry(entries2, options.id) };
2832
- if (options.dryRun === true) return { ok: true, value: { root, manifest: next } };
2833
- try {
2834
- await writeManifest(root, next);
2835
- await mutateDependency(root, "remove", options.dependency);
2836
- return { ok: true, value: { root, id: options.id } };
2837
- } catch (cause) {
2838
- await writeFile(resolve(root, "forge.json"), manifest.value.raw);
2839
- return {
2840
- ok: false,
2841
- error: {
2842
- code: "plugin-uninstall-failed",
2843
- expected: "Entry and dependency removal to commit together",
2844
- hint: "Inspect the package-manager failure; the original forge.json was restored.",
2845
- detail: { reason: cause instanceof Error ? cause.message : String(cause) }
2846
- }
2847
- };
2848
- }
2849
- }
2850
- var execFileAsync3;
2851
- var init_plugin_authoring = __esm({
2852
- "src/plugin-authoring.ts"() {
2853
- execFileAsync3 = promisify(execFile);
2854
- }
2855
- });
2856
- var RhiError;
2857
- var init_dist2 = __esm({
2858
- "../rhi/dist/index.mjs"() {
2859
- RhiError = class extends Error {
2860
- code;
2861
- expected;
2862
- hint;
2863
- detail;
2864
- constructor(args) {
2865
- super(`[RhiError ${args.code}] expected: ${args.expected}; hint: ${args.hint}`);
2866
- this.name = "RhiError";
2867
- this.code = args.code;
2868
- this.expected = args.expected;
2869
- this.hint = args.hint;
2870
- this.detail = args.detail;
2871
- }
2872
- };
2873
- }
2874
- });
2875
- function readPassLabel(desc) {
2876
- if (desc && typeof desc.label === "string" && desc.label.length > 0) {
2877
- return desc.label;
2878
- }
2879
- return "<unnamed>";
2880
- }
2881
- function readRecord(handle) {
2882
- if (handle === null || typeof handle !== "object") return void 0;
2883
- const tagged = handle;
2884
- return tagged[BOOKKEEPING_KEY];
2885
- }
2886
- function isHandleDestroyed(handle) {
2887
- if (readRecord(handle)?.destroyed === true) {
2888
- return err(
2889
- new RhiError({
2890
- code: "destroy-after-destroy",
2891
- expected: "GPU buffer/texture handle has not been destroyed yet",
2892
- hint: "object already destroyed; track lifecycle in caller or check isDestroyed before re-destroy"
2893
- })
2894
- );
2895
- }
2896
- return ok(void 0);
2897
- }
2898
- function acquireCanvasContext(_canvas) {
2899
- return ok(new RhiNullCanvasContext());
2900
- }
2901
- function createShaderModule(_device, _desc) {
2902
- return Promise.resolve(ok({}));
2903
- }
2904
- function requestAdapter(_opts, _compatibleSurface) {
2905
- return Promise.resolve(ok(new RhiNullAdapter()));
2906
- }
2907
- var RhiNullRenderPassEncoder, RhiNullComputePassEncoder, DeviceCounter, RhiNullCommandEncoder, BOOKKEEPING_KEY, Bookkeeper, nextDeviceId, RhiNullDevice, EMPTY_FEATURES, EMPTY_LIMITS, NEVER, RhiNullQueue, RhiNullAdapter, RhiNullCanvasContext, rhi;
2908
- var init_dist3 = __esm({
2909
- "../rhi-null/dist/index.mjs"() {
2910
- init_dist2();
2911
- RhiNullRenderPassEncoder = class {
2912
- /** Number of draw* calls issued on this pass (AC-06 readback). */
2913
- drawCount = 0;
2914
- bindGroupCount = 0;
2915
- /** Most recent setVertexBuffer / setBindGroup ownership validation; ok unless
2916
- * a foreign handle was passed (AC-09 readback). */
2917
- lastValidation = ok(void 0);
2918
- bookkeeper;
2919
- counter;
2920
- passName;
2921
- constructor(bookkeeper, counter, passName) {
2922
- this.bookkeeper = bookkeeper;
2923
- this.counter = counter;
2924
- this.passName = passName;
2925
- }
2926
- setPipeline(_pipeline) {
2927
- }
2928
- setVertexBuffer(_slot, buffer, _offset, _size) {
2929
- this.lastValidation = this.bookkeeper.validateOwnership(buffer);
2930
- }
2931
- setIndexBuffer(_buffer, _format, _offset, _size) {
2932
- }
2933
- setBindGroup(_index, bindGroup, _dynamicOffsetsData, _dynamicOffsetsDataStart, _dynamicOffsetsDataLength) {
2934
- this.bindGroupCount++;
2935
- this.counter?.recordBindGroup();
2936
- this.lastValidation = this.bookkeeper.validateOwnership(bindGroup);
2937
- }
2938
- draw(_vertexCount, _instanceCount, _firstVertex, _firstInstance) {
2939
- this.drawCount++;
2940
- this.counter?.recordDraw();
2941
- }
2942
- drawIndexed(_indexCount, _instanceCount, _firstIndex, _baseVertex, _firstInstance) {
2943
- this.drawCount++;
2944
- this.counter?.recordDraw();
2945
- }
2946
- end() {
2947
- this.counter?.recordPassName(this.passName);
2948
- }
2949
- setViewport(_x, _y, _w, _h, _minDepth, _maxDepth) {
2950
- }
2951
- setScissorRect(_x, _y, _w, _h) {
2952
- }
2953
- setBlendConstant(_color) {
2954
- }
2955
- setStencilReference(_reference) {
2956
- }
2957
- drawIndirect(_indirectBuffer, _indirectOffset) {
2958
- this.drawCount++;
2959
- this.counter?.recordDraw();
2960
- }
2961
- drawIndexedIndirect(_indirectBuffer, _indirectOffset) {
2962
- this.drawCount++;
2963
- this.counter?.recordDraw();
2964
- }
2965
- pushDebugGroup(_groupLabel) {
2966
- }
2967
- popDebugGroup() {
2968
- }
2969
- insertDebugMarker(_markerLabel) {
2970
- }
2971
- executeBundles(_bundles) {
2972
- return ok(void 0);
2973
- }
2974
- beginOcclusionQuery(_queryIndex) {
2975
- return ok(void 0);
2976
- }
2977
- endOcclusionQuery() {
2978
- return ok(void 0);
2979
- }
2980
- };
2981
- RhiNullComputePassEncoder = class {
2982
- /** Number of dispatchWorkgroups calls issued on this pass (readback). */
2983
- dispatchCount = 0;
2984
- /** Most recent setBindGroup ownership validation (AC-09 readback). */
2985
- lastValidation = ok(void 0);
2986
- bookkeeper;
2987
- counter;
2988
- passName;
2989
- constructor(bookkeeper, counter, passName) {
2990
- this.bookkeeper = bookkeeper;
2991
- this.counter = counter;
2992
- this.passName = passName;
2993
- }
2994
- setPipeline(_pipeline) {
2995
- }
2996
- setBindGroup(_index, bindGroup, _dynamicOffsets) {
2997
- this.lastValidation = this.bookkeeper.validateOwnership(bindGroup);
2998
- }
2999
- dispatchWorkgroups(_x, _y, _z) {
3000
- this.dispatchCount++;
3001
- this.counter?.recordDispatch();
3002
- }
3003
- dispatchWorkgroupsIndirect(indirectBuffer, _indirectOffset) {
3004
- this.lastValidation = this.bookkeeper.validateOwnership(indirectBuffer);
3005
- this.dispatchCount++;
3006
- this.counter?.recordDispatch();
3007
- }
3008
- end() {
3009
- this.counter?.recordPassName(this.passName);
3010
- }
3011
- };
3012
- DeviceCounter = class {
3013
- constructor(device) {
3014
- this.device = device;
3015
- }
3016
- device;
3017
- recordDraw() {
3018
- this.device.totalDrawCount++;
3019
- }
3020
- recordDispatch() {
3021
- this.device.totalDispatchCount++;
3022
- }
3023
- recordBindGroup() {
3024
- this.device.totalBindGroupCount++;
3025
- }
3026
- recordPassName(name) {
3027
- this.device.framePassNames.push(name);
3028
- }
3029
- };
3030
- RhiNullCommandEncoder = class {
3031
- bookkeeper;
3032
- counter;
3033
- constructor(bookkeeper, device) {
3034
- this.bookkeeper = bookkeeper;
3035
- this.counter = new DeviceCounter(device);
3036
- }
3037
- beginRenderPass(desc) {
3038
- const label = readPassLabel(desc);
3039
- return new RhiNullRenderPassEncoder(this.bookkeeper, this.counter, label);
3040
- }
3041
- beginComputePass(desc) {
3042
- const label = readPassLabel(desc);
3043
- return new RhiNullComputePassEncoder(this.bookkeeper, this.counter, label);
3044
- }
3045
- copyBufferToBuffer(_source, _sourceOffsetOrDestination, _destinationOrSize, _destinationOffset, _size) {
3046
- }
3047
- copyBufferToTexture(_source, _destination, _copySize) {
3048
- }
3049
- copyTextureToBuffer(_source, _destination, _copySize) {
3050
- }
3051
- copyTextureToTexture(_source, _destination, _copySize) {
3052
- }
3053
- clearBuffer(_buffer, _offset, _size) {
3054
- }
3055
- resolveQuerySet(_querySet, _firstQuery, _queryCount, _destination, _destinationOffset) {
3056
- return ok(void 0);
3057
- }
3058
- writeTimestamp(_querySet, _queryIndex) {
3059
- }
3060
- pushDebugGroup(_groupLabel) {
3061
- }
3062
- popDebugGroup() {
3063
- }
3064
- insertDebugMarker(_markerLabel) {
3065
- }
3066
- finish() {
3067
- return ok(this.bookkeeper.register("CommandBuffer"));
3068
- }
3069
- };
3070
- BOOKKEEPING_KEY = /* @__PURE__ */ Symbol("forgeax-rhi-null-bookkeeping");
3071
- Bookkeeper = class {
3072
- deviceId;
3073
- nextHandleId = 0;
3074
- records = /* @__PURE__ */ new Map();
3075
- constructor(deviceId) {
3076
- this.deviceId = deviceId;
3077
- }
3078
- /**
3079
- * Register a freshly-minted handle of the given kind, returning a plain
3080
- * object that carries its ledger row. The caller casts the return value to
3081
- * the concrete brand (`as unknown as Buffer` etc.).
3082
- */
3083
- register(kind) {
3084
- const id = this.nextHandleId++;
3085
- const record = {
3086
- id,
3087
- kind,
3088
- destroyed: false,
3089
- sourceDeviceId: this.deviceId
3090
- };
3091
- this.records.set(id, record);
3092
- return { [BOOKKEEPING_KEY]: record };
3093
- }
3094
- /**
3095
- * Mark a handle destroyed. Fail-fasts on a second destroy
3096
- * ('destroy-after-destroy') or on a handle issued by a different device
3097
- * ('rhi-not-available'); otherwise flips the destroyed flag and returns ok.
3098
- */
3099
- destroy(handle) {
3100
- const ownership = this.validateOwnership(handle);
3101
- if (!ownership.ok) return ownership;
3102
- const destroyedCheck = isHandleDestroyed(handle);
3103
- if (!destroyedCheck.ok) return destroyedCheck;
3104
- ownership.value.destroyed = true;
3105
- return ok(void 0);
3106
- }
3107
- /**
3108
- * Report whether a handle has already been destroyed (true once destroy()
3109
- * has flipped its flag). Used by command-stream methods that must not
3110
- * consume a stale handle.
3111
- */
3112
- isDestroyed(handle) {
3113
- return readRecord(handle)?.destroyed === true;
3114
- }
3115
- /**
3116
- * Validate that a handle was issued by THIS device. Returns the ledger row on
3117
- * success so callers can mutate it (e.g. flip destroyed); returns
3118
- * 'rhi-not-available' for a foreign handle (AC-09 — no silent pass).
3119
- */
3120
- validateOwnership(handle) {
3121
- const record = readRecord(handle);
3122
- if (record === void 0 || record.sourceDeviceId !== this.deviceId) {
3123
- return err(
3124
- new RhiError({
3125
- code: "rhi-not-available",
3126
- expected: "handle was issued by this RhiNull device",
3127
- hint: "do not pass a handle created on a different RhiNull device into this device; create resources on the device they are used with"
3128
- })
3129
- );
3130
- }
3131
- return ok(record);
3132
- }
3133
- /**
3134
- * Return all ledger rows as a readonly array. M3 unit tests (w17) read this
3135
- * to assert create/destroy pairing, BGL/PSO shape counts, and resource
3136
- * lifecycle coverage (AC-05/06/07). Returns a snapshot of the current Map
3137
- * so callers can iterate without a stale reference after further mutations.
3138
- */
3139
- allRecords() {
3140
- return [...this.records.values()];
3141
- }
3142
- /** Report the total number of records in the ledger. */
3143
- recordCount() {
3144
- return this.records.size;
3145
- }
3146
- };
3147
- nextDeviceId = 0;
3148
- RhiNullDevice = class {
3149
- internalBookkeeper;
3150
- nullQueue;
3151
- encoderFactory;
3152
- /** Per-frame total draw count across all pass encoders executed this frame
3153
- * (aggregated by the command encoder on finish, then reset). M3 unit tests
3154
- * (w17) read this to assert draw count >= 1 (AC-06). */
3155
- totalDrawCount = 0;
3156
- /** Per-frame total direct and indirect compute dispatch count. */
3157
- totalDispatchCount = 0;
3158
- /** Per-frame total bind group set count (AC-06 / AC-05 readback). */
3159
- totalBindGroupCount = 0;
3160
- /** Per-frame pass names executed this frame, in schedule order (AC-04). */
3161
- framePassNames = [];
3162
- /** The per-device handle ledger — exposed so M3 tests can assert create/destroy
3163
- * pairing and BGL/PSO shape counts (AC-05/06/07). */
3164
- get bookkeeper() {
3165
- return this.internalBookkeeper;
3166
- }
3167
- constructor(queue, encoderFactory) {
3168
- this.internalBookkeeper = new Bookkeeper(nextDeviceId++);
3169
- this.nullQueue = queue;
3170
- this.encoderFactory = encoderFactory;
3171
- }
3172
- get caps() {
3173
- return {
3174
- backendKind: "null",
3175
- compute: true,
3176
- timestampQuery: false,
3177
- timestampPeriodNanoseconds: null,
3178
- indirectDrawing: true,
3179
- textureCompressionBc: false,
3180
- textureCompressionEtc2: false,
3181
- textureCompressionAstc: false,
3182
- // 3 wgpu-native-only reserved flags stay false on non-native backends
3183
- // (D-5); the headless backend is not a native runtime.
3184
- multiDrawIndirect: false,
3185
- pushConstants: false,
3186
- textureBindingArray: false,
3187
- samplerAliasing: true,
3188
- firstInstanceIndirect: true,
3189
- storageBuffer: true,
3190
- storageTexture: true,
3191
- rgba16floatRenderable: true,
3192
- rg11b10ufloatRenderable: true,
3193
- float32Filterable: true,
3194
- maxColorAttachments: 8
3195
- };
3196
- }
3197
- get features() {
3198
- return EMPTY_FEATURES;
3199
- }
3200
- get limits() {
3201
- return EMPTY_LIMITS;
3202
- }
3203
- get queue() {
3204
- return this.nullQueue;
3205
- }
3206
- // forgeax-async-whitelist: dom-native — spec `GPUDevice.lost` Promise
3207
- // passthrough. The headless backend never loses a device (no GPU), so the
3208
- // Promise stays unsettled for the lifetime of the device, mirroring a live
3209
- // device that never transitions to the lost state.
3210
- get lost() {
3211
- return NEVER;
3212
- }
3213
- createBuffer(_desc) {
3214
- return ok(this.internalBookkeeper.register("Buffer"));
3215
- }
3216
- createTexture(_desc) {
3217
- return ok(this.internalBookkeeper.register("Texture"));
3218
- }
3219
- destroyBuffer(buf) {
3220
- return this.internalBookkeeper.destroy(buf);
3221
- }
3222
- destroyQuerySet(querySet) {
3223
- return this.internalBookkeeper.destroy(querySet);
3224
- }
3225
- destroyTexture(tex) {
3226
- return this.internalBookkeeper.destroy(tex);
3227
- }
3228
- createTextureView(_texture, _desc) {
3229
- return ok(this.internalBookkeeper.register("TextureView"));
3230
- }
3231
- createSampler(_desc) {
3232
- return ok(this.internalBookkeeper.register("Sampler"));
3233
- }
3234
- createBindGroupLayout(_desc) {
3235
- return ok(this.internalBookkeeper.register("BindGroupLayout"));
3236
- }
3237
- createBindGroup(_desc) {
3238
- return ok(this.internalBookkeeper.register("BindGroup"));
3239
- }
3240
- createPipelineLayout(_desc) {
3241
- return ok(this.internalBookkeeper.register("PipelineLayout"));
3242
- }
3243
- createRenderPipeline(_desc) {
3244
- return ok(this.makePipeline("RenderPipeline"));
3245
- }
3246
- createComputePipeline(_desc) {
3247
- return ok(this.makePipeline("ComputePipeline"));
3248
- }
3249
- createQuerySet(desc) {
3250
- if (desc.type === "timestamp") {
3251
- return err(
3252
- new RhiError({
3253
- code: "feature-not-enabled",
3254
- expected: "caps.timestampQuery === true (timestamp-query feature)",
3255
- hint: "RhiNull is structural-only and cannot produce GPU timestamp ticks"
3256
- })
3257
- );
3258
- }
3259
- return ok(this.internalBookkeeper.register("QuerySet"));
3260
- }
3261
- createCommandEncoder(_desc) {
3262
- return ok(this.encoderFactory(this.internalBookkeeper, this));
3263
- }
3264
- /**
3265
- * Mint a pipeline handle whose object also carries the no-op
3266
- * `getBindGroupLayout(index)` ops method (D-2). The prod auto-layout path
3267
- * (debug-draw.ts) and the existing mock unit tests both call
3268
- * `pipeline.getBindGroupLayout(n)`; returning a legal BindGroupLayout brand
3269
- * (recorded in the ledger) keeps those consumers from crashing on a missing
3270
- * method.
3271
- */
3272
- makePipeline(kind) {
3273
- const handle = this.internalBookkeeper.register(kind);
3274
- const getBindGroupLayout = (_index) => this.internalBookkeeper.register("BindGroupLayout");
3275
- return Object.assign(handle, { getBindGroupLayout });
3276
- }
3277
- };
3278
- EMPTY_FEATURES = /* @__PURE__ */ new Set();
3279
- EMPTY_LIMITS = {};
3280
- NEVER = new Promise(() => {
3281
- });
3282
- RhiNullQueue = class {
3283
- writeBuffer(_buffer, _bufferOffset, _data, _dataOffset, _size) {
3284
- return ok(void 0);
3285
- }
3286
- writeTexture(_destination, _data, _dataLayout, _size) {
3287
- return ok(void 0);
3288
- }
3289
- copyExternalImageToTexture(_source, _destination, _copySize) {
3290
- return ok(void 0);
3291
- }
3292
- submit(_commandBuffers) {
3293
- return ok(void 0);
3294
- }
3295
- // forgeax-async-whitelist: dom-native — spec `GPUQueue.onSubmittedWorkDone`
3296
- // never rejects. The headless backend has no pending GPU work, so it resolves
3297
- // immediately (AC-12: read-back idioms must not hang).
3298
- onSubmittedWorkDone() {
3299
- return Promise.resolve(void 0);
3300
- }
3301
- };
3302
- RhiNullAdapter = class {
3303
- features = /* @__PURE__ */ new Set();
3304
- limits = {};
3305
- // forgeax-async-whitelist is not needed: this returns Promise<Result<...>>
3306
- // per the spec contract; never rejects.
3307
- requestDevice(_opts) {
3308
- const device = new RhiNullDevice(
3309
- new RhiNullQueue(),
3310
- (bookkeeper, dev) => new RhiNullCommandEncoder(bookkeeper, dev)
3311
- );
3312
- return Promise.resolve(ok(device));
3313
- }
3314
- };
3315
- RhiNullCanvasContext = class {
3316
- configure(_desc) {
3317
- return ok(void 0);
3318
- }
3319
- unconfigure() {
3320
- }
3321
- getConfiguration() {
3322
- return void 0;
3379
+ }
3380
+ }
3381
+ async function pluginUninstallCommand(options) {
3382
+ const root = resolve(options.root ?? process.cwd());
3383
+ const manifest = await readManifest2(root);
3384
+ if (!manifest.ok) return manifest;
3385
+ const parsed = GameProjectSchema.safeParse(manifest.value.value);
3386
+ if (!parsed.success) {
3387
+ return {
3388
+ ok: false,
3389
+ error: {
3390
+ code: "plugin-manifest-invalid",
3391
+ expected: "forge.json to satisfy GameProjectSchema",
3392
+ hint: "Repair the manifest before uninstalling a plugin.",
3393
+ detail: { issues: parsed.error.issues }
3323
3394
  }
3324
- getCurrentTexture() {
3325
- return ok({});
3395
+ };
3396
+ }
3397
+ const entries2 = parsed.data.plugins ?? [];
3398
+ if (!allEntries(entries2).some((entry) => entry.id === options.id)) {
3399
+ return {
3400
+ ok: false,
3401
+ error: {
3402
+ code: "plugin-entry-missing",
3403
+ expected: "the plugin Entry id to exist",
3404
+ hint: "Inspect forge.json#plugins and pass an installed Entry id.",
3405
+ detail: { id: options.id }
3326
3406
  }
3327
3407
  };
3328
- rhi = {
3329
- requestAdapter,
3330
- acquireCanvasContext,
3331
- createShaderModule
3408
+ }
3409
+ const next = { ...parsed.data, plugins: withoutEntry(entries2, options.id) };
3410
+ if (options.dryRun === true) return { ok: true, value: { root, manifest: next } };
3411
+ try {
3412
+ await writeManifest(root, next);
3413
+ await mutateDependency(root, "remove", options.dependency);
3414
+ return { ok: true, value: { root, id: options.id } };
3415
+ } catch (cause) {
3416
+ await writeFile(resolve(root, "forge.json"), manifest.value.raw);
3417
+ return {
3418
+ ok: false,
3419
+ error: {
3420
+ code: "plugin-uninstall-failed",
3421
+ expected: "Entry and dependency removal to commit together",
3422
+ hint: "Inspect the package-manager failure; the original forge.json was restored.",
3423
+ detail: { reason: cause instanceof Error ? cause.message : String(cause) }
3424
+ }
3332
3425
  };
3333
3426
  }
3427
+ }
3428
+ var execFileAsync3;
3429
+ var init_plugin_authoring = __esm({
3430
+ "src/plugin-authoring.ts"() {
3431
+ execFileAsync3 = promisify(execFile);
3432
+ }
3334
3433
  });
3335
3434
  function createCliRhiDebugOperationContext() {
3336
3435
  return {
3337
3436
  captureFrame: async () => err(
3338
3437
  createRhiDebugError("capture-unavailable", {
3339
3438
  stage: "capture",
3340
- cause: "the standalone CLI has no live App capture provider"
3439
+ cause: "the standalone CLI has no live App capture provider; start a recorder-enabled live host and invoke its rhiCapture root"
3341
3440
  })
3342
3441
  ),
3343
3442
  async readArtifact(artifact) {
@@ -3369,26 +3468,62 @@ function createCliRhiDebugOperationContext() {
3369
3468
  };
3370
3469
  }
3371
3470
  },
3372
- async createReplayBackend() {
3471
+ async createReplayBackend(tape) {
3472
+ let createDawn;
3473
+ let gpuGlobals;
3474
+ try {
3475
+ const dawn = await import('webgpu');
3476
+ createDawn = dawn.create;
3477
+ gpuGlobals = dawn.globals;
3478
+ } catch (cause) {
3479
+ return {
3480
+ ok: false,
3481
+ error: {
3482
+ code: "replay-backend-unavailable",
3483
+ expected: "the Dawn WebGPU provider to load",
3484
+ hint: "Install the DevKit runtime closure and retry rhi.inspect.",
3485
+ detail: {
3486
+ stage: "provider",
3487
+ cause: cause instanceof Error ? cause.message : String(cause)
3488
+ }
3489
+ }
3490
+ };
3491
+ }
3492
+ Object.assign(globalThis, gpuGlobals);
3493
+ const gpu = createDawn([]);
3494
+ if (!("navigator" in globalThis) || globalThis.navigator === void 0) {
3495
+ Object.defineProperty(globalThis, "navigator", {
3496
+ value: {},
3497
+ configurable: true,
3498
+ writable: true
3499
+ });
3500
+ }
3501
+ Object.defineProperty(globalThis.navigator, "gpu", {
3502
+ value: gpu,
3503
+ configurable: true,
3504
+ writable: true
3505
+ });
3373
3506
  const adapter = await rhi.requestAdapter();
3374
3507
  if (!adapter.ok) {
3375
3508
  return {
3376
3509
  ok: false,
3377
3510
  error: {
3378
3511
  code: "replay-backend-unavailable",
3379
- expected: "a fresh rhi-null adapter",
3512
+ expected: "a fresh Dawn WebGPU adapter",
3380
3513
  hint: adapter.error.hint,
3381
3514
  detail: { stage: "adapter", cause: adapter.error.hint }
3382
3515
  }
3383
3516
  };
3384
3517
  }
3385
- const device = await adapter.value.requestDevice();
3518
+ const device = await adapter.value.requestDevice(
3519
+ replayDeviceRequest(tape, adapter.value.features, adapter.value.limits)
3520
+ );
3386
3521
  if (!device.ok) {
3387
3522
  return {
3388
3523
  ok: false,
3389
3524
  error: {
3390
3525
  code: "replay-backend-unavailable",
3391
- expected: "a fresh rhi-null device",
3526
+ expected: "a fresh Dawn WebGPU device satisfying the recorded tape",
3392
3527
  hint: device.error.hint,
3393
3528
  detail: { stage: "device", cause: device.error.hint }
3394
3529
  }
@@ -3400,7 +3535,6 @@ function createCliRhiDebugOperationContext() {
3400
3535
  }
3401
3536
  var init_cli_context = __esm({
3402
3537
  "src/rhi-debug/cli-context.ts"() {
3403
- init_dist3();
3404
3538
  }
3405
3539
  });
3406
3540
  function createRhiDebugOperationContext(host) {
@@ -3427,6 +3561,9 @@ function renderRhiDebugHelp() {
3427
3561
  return [
3428
3562
  "forgeax run <operation>",
3429
3563
  ...discoverRhiDebugOperations().map((operation) => ` ${operation.name}: ${operation.summary}`),
3564
+ "Usage:",
3565
+ " forgeax run rhi.summary --artifact PATH --digest SHA256 --json",
3566
+ " forgeax run rhi.inspect --artifact PATH --digest SHA256 --work-index N --fields pipeline,bindings,pixels --json",
3430
3567
  "ArtifactRef schema:",
3431
3568
  JSON.stringify(RHI_DEBUG_OPERATION_MANIFEST.artifactRefSchema)
3432
3569
  ].join("\n");
@@ -3554,7 +3691,7 @@ async function runRhiDebugOperation(name, input, context) {
3554
3691
  "Provide a fresh device and shader factory before running rhi.inspect."
3555
3692
  );
3556
3693
  }
3557
- const backend = await context.createReplayBackend();
3694
+ const backend = await context.createReplayBackend(decoded.value);
3558
3695
  if (!backend.ok) return backend;
3559
3696
  const opened = await openReplay(decoded.value, backend.value);
3560
3697
  if (!opened.ok) return { ok: false, error: coreError(opened.error) };
@@ -3646,7 +3783,10 @@ var init_operations = __esm({
3646
3783
  properties: {
3647
3784
  artifact: artifactRefSchema,
3648
3785
  workIndex: { type: "integer", minimum: 0 },
3649
- fields: { type: "array", items: { type: "string" } }
3786
+ fields: {
3787
+ type: "array",
3788
+ items: { type: "string", enum: ["bindings", "pipeline", "pixels"] }
3789
+ }
3650
3790
  },
3651
3791
  required: ["artifact", "workIndex"],
3652
3792
  additionalProperties: false
@@ -3738,7 +3878,15 @@ async function sdkInstallCommand(options) {
3738
3878
  await rename(resolve(staging, name), resolve(root, name));
3739
3879
  }
3740
3880
  }
3741
- return { ok: true, value: { root, sdkVersion: version, source: "@forgeax/engine-sdk" } };
3881
+ return {
3882
+ ok: true,
3883
+ value: {
3884
+ root,
3885
+ sdkVersion: version,
3886
+ source: "@forgeax/engine-sdk",
3887
+ next: { cwd: root, argv: ["node", "./bin/forgeax.mjs", "init"] }
3888
+ }
3889
+ };
3742
3890
  } catch (cause) {
3743
3891
  return { ok: false, error: commandError(cause, "sdk-install-failed") };
3744
3892
  } finally {
@@ -3787,6 +3935,639 @@ var init_shader_check = __esm({
3787
3935
  init_project();
3788
3936
  }
3789
3937
  });
3938
+ function fail(code, expected, hint, detail = {}) {
3939
+ throw new SoftwareCaptureError(code, expected, hint, detail);
3940
+ }
3941
+ async function pathExists2(path) {
3942
+ try {
3943
+ await access(path);
3944
+ return true;
3945
+ } catch {
3946
+ return false;
3947
+ }
3948
+ }
3949
+ async function reservePort() {
3950
+ const reservation = createServer$1();
3951
+ await new Promise((resolveListen, rejectListen) => {
3952
+ reservation.once("error", rejectListen);
3953
+ reservation.listen(0, "127.0.0.1", resolveListen);
3954
+ });
3955
+ const address = reservation.address();
3956
+ const port = typeof address === "object" && address !== null ? address.port : 0;
3957
+ await new Promise((resolveClose, rejectClose) => {
3958
+ reservation.close((error) => error === void 0 ? resolveClose() : rejectClose(error));
3959
+ });
3960
+ if (port === 0) throw new Error("OS did not assign an ephemeral capture port");
3961
+ return port;
3962
+ }
3963
+ async function stopProcess(child) {
3964
+ if (child.exitCode !== null || child.signalCode !== null) return;
3965
+ await new Promise((resolveStop) => {
3966
+ const timeout = setTimeout(() => {
3967
+ child.kill("SIGKILL");
3968
+ resolveStop();
3969
+ }, 2e3);
3970
+ child.once("exit", () => {
3971
+ clearTimeout(timeout);
3972
+ resolveStop();
3973
+ });
3974
+ child.kill("SIGTERM");
3975
+ });
3976
+ }
3977
+ async function startVirtualDisplay(width, height) {
3978
+ if (process.platform !== "linux" || process.env.DISPLAY !== void 0) {
3979
+ return {
3980
+ ...process.env.DISPLAY === void 0 ? {} : { value: process.env.DISPLAY },
3981
+ async close() {
3982
+ }
3983
+ };
3984
+ }
3985
+ for (let offset = 0; offset < 100; offset += 1) {
3986
+ const number = 90 + (process.pid + offset) % 100;
3987
+ const socket = `/tmp/.X11-unix/X${number}`;
3988
+ const lock = `/tmp/.X${number}-lock`;
3989
+ if (await pathExists2(socket) || await pathExists2(lock)) continue;
3990
+ const display = `:${number}`;
3991
+ const child = spawn(
3992
+ "Xvfb",
3993
+ [display, "-screen", "0", `${width}x${height}x24`, "-nolisten", "tcp"],
3994
+ { stdio: ["ignore", "ignore", "pipe"] }
3995
+ );
3996
+ let diagnostic = "";
3997
+ child.stderr?.setEncoding("utf8");
3998
+ child.stderr?.on("data", (chunk) => {
3999
+ diagnostic += chunk;
4000
+ });
4001
+ try {
4002
+ await new Promise((resolveReady, rejectReady) => {
4003
+ const timeout = setTimeout(
4004
+ () => rejectReady(new Error(`Xvfb did not create ${socket}: ${diagnostic.trim()}`)),
4005
+ 5e3
4006
+ );
4007
+ const poll = setInterval(() => {
4008
+ void pathExists2(socket).then((exists) => {
4009
+ if (!exists) return;
4010
+ clearTimeout(timeout);
4011
+ clearInterval(poll);
4012
+ resolveReady();
4013
+ });
4014
+ }, 50);
4015
+ child.once("error", (error) => {
4016
+ clearTimeout(timeout);
4017
+ clearInterval(poll);
4018
+ rejectReady(error);
4019
+ });
4020
+ child.once("exit", (code, signal) => {
4021
+ clearTimeout(timeout);
4022
+ clearInterval(poll);
4023
+ rejectReady(
4024
+ new Error(`Xvfb exited before ready (${code ?? signal}): ${diagnostic.trim()}`)
4025
+ );
4026
+ });
4027
+ });
4028
+ return { value: display, close: () => stopProcess(child) };
4029
+ } catch (cause) {
4030
+ await stopProcess(child);
4031
+ throw cause;
4032
+ }
4033
+ }
4034
+ throw new Error("no free X11 display number was available for browser capture");
4035
+ }
4036
+ async function lavapipeIcd() {
4037
+ for (const candidate of [
4038
+ "/usr/share/vulkan/icd.d/lvp_icd.x86_64.json",
4039
+ "/usr/share/vulkan/icd.d/lvp_icd.aarch64.json"
4040
+ ]) {
4041
+ if (await pathExists2(candidate)) return candidate;
4042
+ }
4043
+ return void 0;
4044
+ }
4045
+ function evidencePath(output) {
4046
+ return output.toLowerCase().endsWith(".png") ? `${output.slice(0, -4)}.json` : `${output}.json`;
4047
+ }
4048
+ function newRunId() {
4049
+ return `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
4050
+ }
4051
+ function checkpointSlug(checkpoint) {
4052
+ const value = (checkpoint ?? "capture").trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
4053
+ return value.length === 0 ? "capture" : value;
4054
+ }
4055
+ function captureDigest(bytes) {
4056
+ return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
4057
+ }
4058
+ function observedBackend(runtime) {
4059
+ if (runtime.adapter === null) return "unknown";
4060
+ return isSoftwareAdapter(runtime) ? "software" : "hardware";
4061
+ }
4062
+ async function resolveBrowserExecutable(requested) {
4063
+ const candidates = [
4064
+ requested,
4065
+ process.env.FORGEAX_BROWSER_EXECUTABLE,
4066
+ "/opt/google/chrome-beta/chrome",
4067
+ "/usr/bin/google-chrome",
4068
+ "/usr/bin/google-chrome-stable",
4069
+ "/usr/bin/chromium",
4070
+ "/usr/bin/chromium-browser"
4071
+ ].filter((candidate) => candidate !== void 0 && candidate.length > 0);
4072
+ for (const candidate of candidates) {
4073
+ if (await pathExists2(candidate)) return candidate;
4074
+ }
4075
+ return void 0;
4076
+ }
4077
+ function browserLaunchArgs(backend) {
4078
+ const common = [
4079
+ "--enable-unsafe-webgpu",
4080
+ "--ignore-gpu-blocklist",
4081
+ "--disable-gpu-driver-bug-workarounds",
4082
+ "--force-color-profile=srgb",
4083
+ "--force-device-scale-factor=1"
4084
+ ];
4085
+ if (backend !== "software") return common;
4086
+ return [
4087
+ ...common,
4088
+ "--enable-features=Vulkan",
4089
+ "--use-vulkan=swiftshader",
4090
+ "--use-angle=swiftshader",
4091
+ "--disable-vulkan-surface"
4092
+ ];
4093
+ }
4094
+ function summarizeCapturePixels(rgba, width, height) {
4095
+ const pixelCount = width * height;
4096
+ const stride = Math.max(1, Math.floor(pixelCount / 4096));
4097
+ const histogram = new Uint32Array(16);
4098
+ let lumaMin = 255;
4099
+ let lumaMax = 0;
4100
+ let sampledPixels = 0;
4101
+ for (let pixel = 0; pixel < pixelCount; pixel += stride) {
4102
+ const offset = pixel * 4;
4103
+ const red = rgba[offset];
4104
+ const green = rgba[offset + 1];
4105
+ const blue = rgba[offset + 2];
4106
+ if (red === void 0 || green === void 0 || blue === void 0) break;
4107
+ const luma = Math.round((54 * red + 183 * green + 19 * blue) / 256);
4108
+ lumaMin = Math.min(lumaMin, luma);
4109
+ lumaMax = Math.max(lumaMax, luma);
4110
+ const bucket = Math.min(15, Math.floor(luma / 16));
4111
+ histogram[bucket] = (histogram[bucket] ?? 0) + 1;
4112
+ sampledPixels += 1;
4113
+ }
4114
+ const dominantPixels = histogram.reduce((largest, count) => Math.max(largest, count), 0);
4115
+ const varyingPixels = sampledPixels - dominantPixels;
4116
+ const lumaRange = lumaMax - lumaMin;
4117
+ const requiredVariation = Math.min(sampledPixels, Math.max(8, Math.ceil(sampledPixels * 2e-3)));
4118
+ return {
4119
+ width,
4120
+ height,
4121
+ sampledPixels,
4122
+ lumaMin,
4123
+ lumaMax,
4124
+ lumaRange,
4125
+ varyingPixels,
4126
+ rendered: lumaRange >= 8 && varyingPixels >= requiredVariation
4127
+ };
4128
+ }
4129
+ function inspectCapturePng(png) {
4130
+ const decoded = parseImage(png, "image/png", { mipmap: false });
4131
+ if (!decoded.ok) throw decoded.error;
4132
+ return summarizeCapturePixels(decoded.value.bytes, decoded.value.width, decoded.value.height);
4133
+ }
4134
+ async function screenshotWithWitness(page) {
4135
+ const canvasPng = await page.locator("canvas").first().screenshot({
4136
+ type: "png",
4137
+ style: "* { visibility: hidden !important; } canvas { visibility: visible !important; }"
4138
+ });
4139
+ const png = await page.screenshot({ type: "png", caret: "hide" });
4140
+ return { png, pixels: inspectCapturePng(canvasPng) };
4141
+ }
4142
+ async function waitForCompositor(page) {
4143
+ await page.evaluate(async () => {
4144
+ await document.fonts.ready;
4145
+ await new Promise((resolveFrame) => requestAnimationFrame(() => resolveFrame()));
4146
+ await new Promise((resolveFrame) => requestAnimationFrame(() => resolveFrame()));
4147
+ });
4148
+ }
4149
+ async function runtimeWitness(page) {
4150
+ return page.evaluate(async (datasetKey) => {
4151
+ const canvas = document.querySelector("canvas");
4152
+ const uiRoot = document.querySelector("#game-ui");
4153
+ const shadowHosts = [...uiRoot?.querySelectorAll("*") ?? []].filter(
4154
+ (element) => element.shadowRoot?.childElementCount !== 0
4155
+ );
4156
+ let adapter = null;
4157
+ let adapterError = null;
4158
+ try {
4159
+ const gpuAdapter = await navigator.gpu?.requestAdapter();
4160
+ if (gpuAdapter === null || gpuAdapter === void 0) {
4161
+ adapterError = "navigator.gpu.requestAdapter() returned null";
4162
+ } else {
4163
+ const info = gpuAdapter.info;
4164
+ adapter = {
4165
+ vendor: info.vendor,
4166
+ architecture: info.architecture,
4167
+ device: info.device,
4168
+ description: info.description
4169
+ };
4170
+ }
4171
+ } catch (cause) {
4172
+ adapterError = String(cause);
4173
+ }
4174
+ const frameId = Number(document.documentElement.dataset[datasetKey]);
4175
+ return {
4176
+ title: document.title,
4177
+ canvas: canvas instanceof HTMLCanvasElement ? { width: canvas.width, height: canvas.height } : null,
4178
+ domUi: {
4179
+ rootChildren: uiRoot?.childElementCount ?? 0,
4180
+ openShadowRoots: shadowHosts.length,
4181
+ textWitness: shadowHosts.map((host) => host.shadowRoot?.textContent?.replace(/\s+/g, " ").trim() ?? "").filter((text) => text.length > 0).join(" | ").slice(0, 500)
4182
+ },
4183
+ adapter,
4184
+ adapterError,
4185
+ engineFrameId: Number.isSafeInteger(frameId) && frameId > 0 ? frameId : null,
4186
+ captureReady: document.documentElement.dataset.forgeaxCaptureReady ?? null,
4187
+ userAgent: navigator.userAgent
4188
+ };
4189
+ }, FORGEAX_FRAME_SUBMITTED_DATASET);
4190
+ }
4191
+ function isSoftwareAdapter(runtime) {
4192
+ const witness = Object.values(runtime.adapter ?? {}).map((value) => String(value).toLowerCase()).join(" ");
4193
+ return ["swiftshader", "llvmpipe", "lavapipe", "software"].some(
4194
+ (token) => witness.includes(token)
4195
+ );
4196
+ }
4197
+ async function writeRunReport(report) {
4198
+ await mkdir(dirname(report.report), { recursive: true });
4199
+ await writeFile(report.report, `${JSON.stringify(report, null, 2)}
4200
+ `, "utf8");
4201
+ }
4202
+ async function openBrowserCaptureSession(root, options) {
4203
+ const backend = options.backend ?? (options.software === true ? "software" : "auto");
4204
+ if (options.software === true && options.backend !== void 0 && options.backend !== "software") {
4205
+ fail(
4206
+ "browser-capture-option-conflict",
4207
+ "software and backend options to describe the same capture lane",
4208
+ "Use either software: true or backend: software; use backend: auto for a portable capture.",
4209
+ { software: options.software, backend: options.backend }
4210
+ );
4211
+ }
4212
+ const facts = await readProjectFacts(root);
4213
+ if (!facts.ok) {
4214
+ throw new SoftwareCaptureError(
4215
+ facts.error.code,
4216
+ facts.error.expected,
4217
+ facts.error.hint,
4218
+ facts.error.detail
4219
+ );
4220
+ }
4221
+ const width = options.width ?? 1280;
4222
+ const height = options.height ?? 720;
4223
+ const browserPath = await resolveBrowserExecutable(options.browser);
4224
+ if (options.browser !== void 0 && browserPath === void 0) {
4225
+ fail(
4226
+ "browser-capture-browser-missing",
4227
+ `a runnable browser at ${options.browser}`,
4228
+ "Install Chrome/Chromium or pass --browser with an executable path.",
4229
+ { browser: options.browser }
4230
+ );
4231
+ }
4232
+ const id = options.runId ?? newRunId();
4233
+ const outputDirectory = resolve(
4234
+ facts.value.root,
4235
+ options.outputDir ?? `artifacts/playthrough/${id}`
4236
+ );
4237
+ const reportPath = resolve(
4238
+ facts.value.root,
4239
+ options.report ?? resolve(outputDirectory, "run.json")
4240
+ );
4241
+ const port = options.port === void 0 || options.port === 0 ? await reservePort() : options.port;
4242
+ let server;
4243
+ let browser;
4244
+ let page;
4245
+ let display;
4246
+ try {
4247
+ const { createServer: createServer5 } = await import('vite');
4248
+ server = await createServer5(
4249
+ await createViteConfig(facts.value, "serve", "/", {
4250
+ server: { port, strictPort: true }
4251
+ })
4252
+ );
4253
+ await server.listen(port);
4254
+ display = await startVirtualDisplay(width, height);
4255
+ const captureDisplay = display;
4256
+ const icd = await lavapipeIcd();
4257
+ const consoleErrors = [];
4258
+ const pageErrors = [];
4259
+ const hasDisplay = display.value !== void 0;
4260
+ const headless = options.headless ?? (!hasDisplay && process.platform !== "linux");
4261
+ const baseUrl = new URL(server.resolvedUrls?.local[0] ?? `http://127.0.0.1:${port}/`);
4262
+ const openPage = async (launchBackend) => {
4263
+ const browserEnvironment = {
4264
+ ...Object.fromEntries(
4265
+ Object.entries(process.env).filter(
4266
+ (entry) => entry[1] !== void 0
4267
+ )
4268
+ ),
4269
+ ...launchBackend === "software" ? { LIBGL_ALWAYS_SOFTWARE: "1" } : {},
4270
+ ...captureDisplay.value === void 0 ? {} : { DISPLAY: captureDisplay.value }
4271
+ };
4272
+ const launchOptions = {
4273
+ headless,
4274
+ env: browserEnvironment,
4275
+ args: browserLaunchArgs(launchBackend),
4276
+ ...browserPath === void 0 ? {} : { executablePath: browserPath }
4277
+ };
4278
+ browser = await chromium.launch(launchOptions);
4279
+ page = await browser.newPage({
4280
+ viewport: { width, height },
4281
+ screen: { width, height },
4282
+ deviceScaleFactor: 1,
4283
+ colorScheme: "light",
4284
+ locale: "en-US",
4285
+ timezoneId: "UTC",
4286
+ serviceWorkers: "block"
4287
+ });
4288
+ page.on("console", (message) => {
4289
+ if (message.type() === "error") consoleErrors.push(message.text());
4290
+ });
4291
+ page.on("pageerror", (error) => pageErrors.push(String(error)));
4292
+ const captureUrl2 = new URL(baseUrl.href);
4293
+ if (options.deterministic === true) captureUrl2.searchParams.set("forgeaxCapture", "1");
4294
+ await page.goto(captureUrl2.href, { waitUntil: "domcontentloaded", timeout: 12e4 });
4295
+ await page.waitForFunction(
4296
+ (requireUi) => {
4297
+ const canvas = document.querySelector("canvas");
4298
+ if (!(canvas instanceof HTMLCanvasElement) || canvas.width <= 0 || canvas.height <= 0)
4299
+ return false;
4300
+ if (!requireUi) return true;
4301
+ const uiRoot = document.querySelector("#game-ui");
4302
+ return uiRoot !== null && uiRoot.childElementCount > 0;
4303
+ },
4304
+ options.requireUi === true,
4305
+ { timeout: 12e4 }
4306
+ );
4307
+ return runtimeWitness(page);
4308
+ };
4309
+ const initialLaunchBackend = backend === "software" ? "software" : "hardware";
4310
+ let runtime = await openPage(initialLaunchBackend);
4311
+ let observed = observedBackend(runtime);
4312
+ if (backend === "auto" && runtime.adapter === null) {
4313
+ await browser?.close();
4314
+ browser = void 0;
4315
+ page = void 0;
4316
+ consoleErrors.length = 0;
4317
+ pageErrors.length = 0;
4318
+ runtime = await openPage("software");
4319
+ observed = observedBackend(runtime);
4320
+ }
4321
+ const captureUrl = new URL(baseUrl.href);
4322
+ if (options.deterministic === true) captureUrl.searchParams.set("forgeaxCapture", "1");
4323
+ if (page === void 0 || browser === void 0) {
4324
+ fail(
4325
+ "browser-capture-browser-unavailable",
4326
+ "the browser capture session to open",
4327
+ "Inspect the browser launch diagnostic and retry with --browser or --headless."
4328
+ );
4329
+ }
4330
+ const captures = [];
4331
+ let closed = false;
4332
+ const report = {
4333
+ schemaVersion: "2.0.0",
4334
+ runId: id,
4335
+ ok: false,
4336
+ mode: "browser-compositor",
4337
+ root: facts.value.root,
4338
+ url: captureUrl.href,
4339
+ report: reportPath,
4340
+ backendRequested: backend,
4341
+ backend: observed,
4342
+ softwareRequested: backend === "software",
4343
+ deterministicRequested: options.deterministic === true,
4344
+ viewport: {
4345
+ width,
4346
+ height,
4347
+ deviceScaleFactor: 1,
4348
+ colorProfile: "srgb",
4349
+ colorScheme: "light",
4350
+ locale: "en-US",
4351
+ timezone: "UTC"
4352
+ },
4353
+ browser: { version: browser.version(), executable: browserPath ?? "playwright-managed" },
4354
+ display: display.value ?? null,
4355
+ lavapipeIcd: icd ?? null,
4356
+ captures,
4357
+ consoleErrors,
4358
+ pageErrors,
4359
+ boundary: "Browser-compositor capture is visual iteration evidence, not physical-GPU performance, HDR-display output, or release acceptance."
4360
+ };
4361
+ await writeRunReport(report);
4362
+ const updateReport = async () => {
4363
+ Object.assign(report, {
4364
+ ok: captures.length > 0 && captures.every((capture) => capture.ok) && consoleErrors.length === 0 && pageErrors.length === 0
4365
+ });
4366
+ await writeRunReport(report);
4367
+ };
4368
+ const close = async () => {
4369
+ if (closed) return;
4370
+ closed = true;
4371
+ await Promise.allSettled([browser?.close(), display?.close(), server?.close()]);
4372
+ Object.assign(report, { closedAt: (/* @__PURE__ */ new Date()).toISOString() });
4373
+ await updateReport();
4374
+ };
4375
+ return {
4376
+ page,
4377
+ url: captureUrl.href,
4378
+ reportPath,
4379
+ async capture(checkpoint, captureOptions = {}) {
4380
+ if (closed) {
4381
+ fail(
4382
+ "browser-capture-session-closed",
4383
+ "capture to run inside an open browser session",
4384
+ "Open one session, complete all checkpoints, then close it.",
4385
+ { report: reportPath }
4386
+ );
4387
+ }
4388
+ const activePage = page;
4389
+ if (activePage === void 0) {
4390
+ fail(
4391
+ "browser-capture-browser-unavailable",
4392
+ "an open Playwright page",
4393
+ "Inspect the browser launch diagnostic and retry the capture."
4394
+ );
4395
+ }
4396
+ const expectedReady = checkpoint ?? (options.deterministic === true ? "true" : void 0);
4397
+ await activePage.waitForFunction(
4398
+ ({ datasetKey }) => Number(document.documentElement.dataset[datasetKey]) > 0,
4399
+ { datasetKey: FORGEAX_FRAME_SUBMITTED_DATASET },
4400
+ { timeout: 12e4 }
4401
+ );
4402
+ if (expectedReady !== void 0) {
4403
+ await activePage.waitForFunction(
4404
+ (expected) => document.documentElement.dataset.forgeaxCaptureReady === expected,
4405
+ expectedReady,
4406
+ { timeout: 12e4 }
4407
+ );
4408
+ }
4409
+ await waitForCompositor(activePage);
4410
+ let captured = await screenshotWithWitness(activePage);
4411
+ const deadline = Date.now() + 12e4;
4412
+ while (!captured.pixels.rendered && Date.now() < deadline) {
4413
+ await activePage.waitForTimeout(500);
4414
+ await waitForCompositor(activePage);
4415
+ captured = await screenshotWithWitness(activePage);
4416
+ }
4417
+ const waitMs = captureOptions.waitMs ?? 0;
4418
+ if (waitMs > 0) {
4419
+ await activePage.waitForTimeout(waitMs);
4420
+ await waitForCompositor(activePage);
4421
+ captured = await screenshotWithWitness(activePage);
4422
+ while (!captured.pixels.rendered && Date.now() < deadline) {
4423
+ await activePage.waitForTimeout(500);
4424
+ await waitForCompositor(activePage);
4425
+ captured = await screenshotWithWitness(activePage);
4426
+ }
4427
+ }
4428
+ const runtime2 = await runtimeWitness(activePage);
4429
+ const uiPresent = runtime2.domUi.rootChildren > 0;
4430
+ const requireUi = captureOptions.requireUi ?? options.requireUi ?? false;
4431
+ const captureBackend = observedBackend(runtime2);
4432
+ if (captureBackend !== "unknown") Object.assign(report, { backend: captureBackend });
4433
+ const backendMatches = backend === "auto" || captureBackend === backend;
4434
+ 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);
4435
+ const index = captures.length + 1;
4436
+ const output = resolve(
4437
+ facts.value.root,
4438
+ captureOptions.output ?? resolve(
4439
+ outputDirectory,
4440
+ `${String(index).padStart(3, "0")}-${checkpointSlug(checkpoint)}.png`
4441
+ )
4442
+ );
4443
+ await mkdir(dirname(output), { recursive: true });
4444
+ await writeFile(output, captured.png);
4445
+ const record = {
4446
+ index,
4447
+ checkpoint: checkpoint ?? null,
4448
+ ok: ok2,
4449
+ output,
4450
+ digest: captureDigest(captured.png),
4451
+ pixels: captured.pixels,
4452
+ runtime: runtime2
4453
+ };
4454
+ captures.push(record);
4455
+ await updateReport();
4456
+ if (!ok2) {
4457
+ fail(
4458
+ `${backend === "software" ? "software" : "browser"}-capture-runtime-failed`,
4459
+ "non-flat canvas pixels, the requested browser backend/checkpoint, and no browser errors",
4460
+ "Inspect the run report and repair the first browser runtime failure.",
4461
+ { checkpoint: checkpoint ?? null, output, report: reportPath }
4462
+ );
4463
+ }
4464
+ return record;
4465
+ },
4466
+ report: () => report,
4467
+ close
4468
+ };
4469
+ } catch (cause) {
4470
+ await Promise.allSettled([browser?.close(), display?.close(), server?.close()]);
4471
+ throw cause;
4472
+ }
4473
+ }
4474
+ function createBrowserCapture(root) {
4475
+ const sessions = /* @__PURE__ */ new Set();
4476
+ return {
4477
+ async open(options) {
4478
+ const session = await openBrowserCaptureSession(root, options);
4479
+ sessions.add(session);
4480
+ return session;
4481
+ },
4482
+ async close() {
4483
+ await Promise.allSettled([...sessions].map((session) => session.close()));
4484
+ sessions.clear();
4485
+ }
4486
+ };
4487
+ }
4488
+ function createSoftwareBrowser(root) {
4489
+ const browser = createBrowserCapture(root);
4490
+ return {
4491
+ async open(options) {
4492
+ return browser.open({ ...options, backend: "software", software: true });
4493
+ },
4494
+ close: () => browser.close()
4495
+ };
4496
+ }
4497
+ function captureCommandError(cause, legacySoftware = false) {
4498
+ if (cause instanceof SoftwareCaptureError) {
4499
+ const code = legacySoftware && cause.code.startsWith("browser-capture-") ? cause.code.replace(/^browser-capture-/, "software-capture-") : cause.code;
4500
+ return {
4501
+ code,
4502
+ expected: cause.expected,
4503
+ hint: cause.hint,
4504
+ detail: cause.detail
4505
+ };
4506
+ }
4507
+ return commandError(cause, legacySoftware ? "software-capture-failed" : "browser-capture-failed");
4508
+ }
4509
+ async function browserCaptureCommand(options) {
4510
+ const root = options.root ?? process.cwd();
4511
+ const output = resolve(root, options.output ?? "artifacts/capture/game-ui.png");
4512
+ const browser = createBrowserCapture(root);
4513
+ try {
4514
+ const backend = options.backend ?? (options.software === true ? "software" : "auto");
4515
+ const session = await browser.open({
4516
+ backend,
4517
+ ...backend === "software" ? { software: true } : {},
4518
+ ...options.browser === void 0 ? {} : { browser: options.browser },
4519
+ ...options.width === void 0 ? {} : { width: options.width },
4520
+ ...options.height === void 0 ? {} : { height: options.height },
4521
+ ...options.port === void 0 ? {} : { port: options.port },
4522
+ ...options.headless === void 0 ? {} : { headless: options.headless },
4523
+ requireUi: options.requireUi === true,
4524
+ deterministic: options.deterministic === true,
4525
+ outputDir: dirname(output),
4526
+ report: evidencePath(output)
4527
+ });
4528
+ await session.capture(options.deterministic === true ? "true" : void 0, {
4529
+ output,
4530
+ waitMs: options.waitMs ?? 4e3,
4531
+ requireUi: options.requireUi === true
4532
+ });
4533
+ await session.close();
4534
+ return { ok: true, value: session.report() };
4535
+ } catch (cause) {
4536
+ return { ok: false, error: captureCommandError(cause) };
4537
+ } finally {
4538
+ await browser.close();
4539
+ }
4540
+ }
4541
+ async function softwareCaptureCommand(options) {
4542
+ const result = await browserCaptureCommand({ ...options, backend: "software", software: true });
4543
+ if (result.ok) return result;
4544
+ return { ok: false, error: captureCommandErrorFromResult(result.error) };
4545
+ }
4546
+ function captureCommandErrorFromResult(error, legacySoftware) {
4547
+ if (!error.code.startsWith("browser-capture-")) return error;
4548
+ return { ...error, code: error.code.replace(/^browser-capture-/, "software-capture-") };
4549
+ }
4550
+ var SoftwareCaptureError;
4551
+ var init_software_capture = __esm({
4552
+ "src/software-capture.ts"() {
4553
+ init_host();
4554
+ init_project();
4555
+ SoftwareCaptureError = class extends Error {
4556
+ constructor(code, expected, hint, detail) {
4557
+ super(`${code}: ${hint}`);
4558
+ this.code = code;
4559
+ this.expected = expected;
4560
+ this.hint = hint;
4561
+ this.detail = detail;
4562
+ this.name = "SoftwareCaptureError";
4563
+ }
4564
+ code;
4565
+ expected;
4566
+ hint;
4567
+ detail;
4568
+ };
4569
+ }
4570
+ });
3790
4571
 
3791
4572
  // src/commands.ts
3792
4573
  var commands_exports = {};
@@ -3796,12 +4577,17 @@ __export(commands_exports, {
3796
4577
  assetInspectCommand: () => assetInspectCommand,
3797
4578
  assetListCommand: () => assetListCommand,
3798
4579
  assetVerifyCommand: () => assetVerifyCommand,
4580
+ browserCaptureCommand: () => browserCaptureCommand,
3799
4581
  buildCommand: () => buildCommand,
3800
4582
  createCliRhiDebugOperationContext: () => createCliRhiDebugOperationContext,
3801
4583
  createRhiDebugOperationContext: () => createRhiDebugOperationContext,
3802
4584
  devCommand: () => devCommand,
3803
4585
  discoverRhiDebugOperations: () => discoverRhiDebugOperations,
3804
4586
  doctorCommand: () => doctorCommand,
4587
+ engineDoctorCommand: () => engineDoctorCommand,
4588
+ engineStatusCommand: () => engineStatusCommand,
4589
+ engineUnlinkCommand: () => engineUnlinkCommand,
4590
+ engineUseLocalCommand: () => engineUseLocalCommand,
3805
4591
  initCommand: () => initCommand,
3806
4592
  newCommand: () => newCommand,
3807
4593
  packageCommand: () => packageCommand,
@@ -3816,11 +4602,27 @@ __export(commands_exports, {
3816
4602
  shaderCheckCommand: () => shaderCheckCommand,
3817
4603
  skillInstallCommand: () => skillInstallCommand,
3818
4604
  skillVerifyCommand: () => skillVerifyCommand,
4605
+ softwareCaptureCommand: () => softwareCaptureCommand,
3819
4606
  testCommand: () => testCommand
3820
4607
  });
3821
4608
  function runRhiDebugCommand(name, input, context) {
3822
4609
  return runRhiDebugOperation(name, input, context);
3823
4610
  }
4611
+ async function materializeViteDevPort(port) {
4612
+ if (port !== 0) return port;
4613
+ const reservation = createServer$1();
4614
+ await new Promise((resolveListen, rejectListen) => {
4615
+ reservation.once("error", rejectListen);
4616
+ reservation.listen(0, "127.0.0.1", resolveListen);
4617
+ });
4618
+ const address = reservation.address();
4619
+ const assigned = typeof address === "object" && address !== null ? address.port : 0;
4620
+ await new Promise((resolveClose, rejectClose) => {
4621
+ reservation.close((error) => error === void 0 ? resolveClose() : rejectClose(error));
4622
+ });
4623
+ if (assigned === 0) throw new Error("OS did not assign an ephemeral preview port");
4624
+ return assigned;
4625
+ }
3824
4626
  async function buildCommand(options = {}) {
3825
4627
  const facts = await readProjectFacts(options.root);
3826
4628
  if (!facts.ok) return facts;
@@ -3923,13 +4725,38 @@ async function devCommand(options = {}) {
3923
4725
  const facts = await readProjectFacts(options.root);
3924
4726
  if (!facts.ok) return facts;
3925
4727
  const previous = process.cwd();
4728
+ let server;
3926
4729
  try {
4730
+ const { createServer: createServer5 } = await import('vite');
4731
+ const { createViteConfig: createViteConfig2 } = await Promise.resolve().then(() => (init_host(), host_exports));
4732
+ const port = resolveProjectPort(options.port);
4733
+ const vitePort = { ...port, port: await materializeViteDevPort(port.port) };
3927
4734
  process.chdir(facts.value.root);
3928
- const server = await createServer(await createViteConfig(facts.value, "serve"));
3929
- await server.listen();
4735
+ server = await createServer5(
4736
+ await createViteConfig2(facts.value, "serve", "/", {
4737
+ server: vitePort
4738
+ })
4739
+ );
4740
+ await server.listen(vitePort.port);
3930
4741
  if (options.json !== true) server.printUrls();
3931
- return { ok: true, value: { root: facts.value.root, urls: server.resolvedUrls } };
4742
+ return {
4743
+ ok: true,
4744
+ value: {
4745
+ root: facts.value.root,
4746
+ urls: server.resolvedUrls,
4747
+ mode: "dev",
4748
+ serves: "source",
4749
+ capabilities: {
4750
+ "rhi.capture": {
4751
+ available: false,
4752
+ realm: "host",
4753
+ reason: "standalone-dev-server-has-no-live-app-cli-attachment"
4754
+ }
4755
+ }
4756
+ }
4757
+ };
3932
4758
  } catch (cause) {
4759
+ await server?.close();
3933
4760
  process.chdir(previous);
3934
4761
  return { ok: false, error: commandError(cause, "dev-server-failed") };
3935
4762
  }
@@ -3939,15 +4766,31 @@ async function previewCommand(options = {}) {
3939
4766
  const verified = await verifyDist(resolve(root, "dist"));
3940
4767
  if (!verified.ok) return verified;
3941
4768
  try {
4769
+ const port = resolveProjectPort(options.port);
3942
4770
  const server = await preview({
3943
4771
  root,
3944
4772
  configFile: false,
3945
4773
  base: verified.value.base,
3946
- preview: { open: false, port: 0, strictPort: false },
4774
+ preview: { open: false, ...port },
3947
4775
  build: { outDir: resolve(root, "dist") }
3948
4776
  });
3949
4777
  if (options.json !== true) server.printUrls();
3950
- return { ok: true, value: { root, urls: server.resolvedUrls } };
4778
+ return {
4779
+ ok: true,
4780
+ value: {
4781
+ root,
4782
+ urls: server.resolvedUrls,
4783
+ mode: "preview",
4784
+ serves: "dist",
4785
+ capabilities: {
4786
+ "rhi.capture": {
4787
+ available: false,
4788
+ realm: "host",
4789
+ reason: "static-dist-host-does-not-install-dev-capture"
4790
+ }
4791
+ }
4792
+ }
4793
+ };
3951
4794
  } catch (cause) {
3952
4795
  return { ok: false, error: commandError(cause, "preview-server-failed") };
3953
4796
  }
@@ -3997,14 +4840,17 @@ var init_commands = __esm({
3997
4840
  init_dist();
3998
4841
  init_host();
3999
4842
  init_project();
4843
+ init_types();
4000
4844
  init_assets();
4001
4845
  init_bootstrap_commands();
4846
+ init_engine_binding();
4002
4847
  init_plugin_authoring();
4003
4848
  init_cli_context();
4004
4849
  init_operations();
4005
4850
  init_sdk_install();
4006
4851
  init_shader_check();
4007
4852
  init_skill_install();
4853
+ init_software_capture();
4008
4854
  init_operations();
4009
4855
  }
4010
4856
  });
@@ -4391,7 +5237,7 @@ function hasStaticToolDeclarations(entry) {
4391
5237
  return Array.isArray(Reflect.get(entry.config, "tools"));
4392
5238
  }
4393
5239
  async function createViteModuleLoader(root) {
4394
- const server = await createServer({
5240
+ const server = await createServer$2({
4395
5241
  root,
4396
5242
  appType: "custom",
4397
5243
  configFile: false,
@@ -4651,9 +5497,11 @@ var init_runtime = __esm({
4651
5497
  // src/index.ts
4652
5498
  init_commands();
4653
5499
  init_dist();
5500
+ init_engine_binding();
4654
5501
  init_init();
4655
5502
  init_project();
4656
5503
  init_operations();
5504
+ init_software_capture();
4657
5505
 
4658
5506
  // src/tools/benchmark/statistics.ts
4659
5507
  function calculateSampleStatistics(samples) {
@@ -4773,9 +5621,11 @@ function createAdmissionReport(recipe, samples, thresholds = DEFAULT_ADMISSION_T
4773
5621
  async function runBenchmarkAdmission(options) {
4774
5622
  const sampleCount = options.thresholds?.sampleCountPerPhase ?? 30;
4775
5623
  const samples = [];
4776
- for (const phase of ["cold", "warm"]) {
5624
+ const phases = ["cold", "warm"];
5625
+ const modes = ["private", "service"];
5626
+ for (const phase of phases) {
4777
5627
  for (let index = 0; index < sampleCount; index += 1) {
4778
- for (const mode of ["private", "service"]) {
5628
+ for (const mode of modes) {
4779
5629
  const measurement = await options.measure(mode, phase, index);
4780
5630
  samples.push({
4781
5631
  mode,
@@ -4882,7 +5732,7 @@ function reply(response, status, payload) {
4882
5732
  async function createCarrierProviderService(options) {
4883
5733
  const host = options.host ?? "127.0.0.1";
4884
5734
  const bearerToken = options.machine.offer.bearerToken;
4885
- const server = createServer$1(async (request, response) => {
5735
+ const server = createServer(async (request, response) => {
4886
5736
  if (request.method !== "POST" || !request.url?.startsWith("/carrier/")) {
4887
5737
  reply(
4888
5738
  response,
@@ -5001,14 +5851,14 @@ async function createCarrierProviderService(options) {
5001
5851
  );
5002
5852
  }
5003
5853
  });
5004
- await new Promise((resolve16, reject) => {
5854
+ await new Promise((resolve18, reject) => {
5005
5855
  const onError = (error) => {
5006
5856
  server.off("listening", onListening);
5007
5857
  reject(error);
5008
5858
  };
5009
5859
  const onListening = () => {
5010
5860
  server.off("error", onError);
5011
- resolve16();
5861
+ resolve18();
5012
5862
  };
5013
5863
  server.once("error", onError);
5014
5864
  server.once("listening", onListening);
@@ -5026,7 +5876,7 @@ async function createCarrierProviderService(options) {
5026
5876
  if (closed) return;
5027
5877
  closed = true;
5028
5878
  await new Promise(
5029
- (resolve16, reject) => server.close((error) => error === void 0 ? resolve16() : reject(error))
5879
+ (resolve18, reject) => server.close((error) => error === void 0 ? resolve18() : reject(error))
5030
5880
  );
5031
5881
  }
5032
5882
  };
@@ -5318,16 +6168,16 @@ function projectUri(projectRoot, path) {
5318
6168
  return relative(projectRoot, path).split(sep).join("/");
5319
6169
  }
5320
6170
  async function allocateLoopbackPort() {
5321
- const probe = createServer$2();
6171
+ const probe = createServer$1();
5322
6172
  try {
5323
- await new Promise((resolve16, reject) => {
6173
+ await new Promise((resolve18, reject) => {
5324
6174
  const onError = (error) => {
5325
6175
  probe.off("listening", onListening);
5326
6176
  reject(error);
5327
6177
  };
5328
6178
  const onListening = () => {
5329
6179
  probe.off("error", onError);
5330
- resolve16();
6180
+ resolve18();
5331
6181
  };
5332
6182
  probe.once("error", onError);
5333
6183
  probe.once("listening", onListening);
@@ -5340,7 +6190,7 @@ async function allocateLoopbackPort() {
5340
6190
  return address.port;
5341
6191
  } finally {
5342
6192
  if (probe.listening) {
5343
- await new Promise((resolve16) => probe.close(() => resolve16()));
6193
+ await new Promise((resolve18) => probe.close(() => resolve18()));
5344
6194
  }
5345
6195
  }
5346
6196
  }
@@ -5497,7 +6347,7 @@ async function runBrowserPreviewHost(projectRoot, recipe, snapshot, runId, signa
5497
6347
  let server;
5498
6348
  try {
5499
6349
  const port = await allocateLoopbackPort();
5500
- server = await createServer({
6350
+ server = await createServer$2({
5501
6351
  ...config,
5502
6352
  cacheDir,
5503
6353
  logLevel: "silent",
@@ -5533,6 +6383,7 @@ async function runBrowserPreviewHost(projectRoot, recipe, snapshot, runId, signa
5533
6383
  "--enable-unsafe-webgpu",
5534
6384
  "--enable-features=Vulkan,UseSkiaRenderer,SharedArrayBuffer",
5535
6385
  "--use-vulkan=swiftshader",
6386
+ "--use-angle=swiftshader",
5536
6387
  "--disable-vulkan-surface",
5537
6388
  "--ignore-gpu-blocklist",
5538
6389
  "--disable-gpu-driver-bug-workarounds",
@@ -5619,9 +6470,7 @@ async function runBrowserPreviewHost(projectRoot, recipe, snapshot, runId, signa
5619
6470
  );
5620
6471
  });
5621
6472
  await Promise.all(responseDiagnostics);
5622
- if (!captured.ok) {
5623
- return browserFailure("capture-run", captured.error, pageErrors);
5624
- }
6473
+ if (!captured.ok) return browserFailure("capture-run", captured.error, pageErrors);
5625
6474
  if (pageErrors.length > 0)
5626
6475
  return browserFailure("capture-page-runtime", pageErrors[0], pageErrors);
5627
6476
  const capturePng = await page.screenshot({ type: "png" });
@@ -5962,7 +6811,7 @@ function isAuthorized(request, bearerToken) {
5962
6811
  async function createAuthenticatedLoopbackService(options) {
5963
6812
  if (options.bearerToken.length < 8) throw new TypeError("service bearer token is too short");
5964
6813
  const host = options.host ?? "127.0.0.1";
5965
- const server = createServer$1(async (request, response) => {
6814
+ const server = createServer(async (request, response) => {
5966
6815
  if (request.method !== "POST" || request.url !== "/run") {
5967
6816
  reply2(response, 404, { error: "service route not found" });
5968
6817
  return;
@@ -5987,14 +6836,14 @@ async function createAuthenticatedLoopbackService(options) {
5987
6836
  reply2(response, 400, { error: message });
5988
6837
  }
5989
6838
  });
5990
- await new Promise((resolve16, reject) => {
6839
+ await new Promise((resolve18, reject) => {
5991
6840
  const onError = (error) => {
5992
6841
  server.off("listening", onListening);
5993
6842
  reject(error);
5994
6843
  };
5995
6844
  const onListening = () => {
5996
6845
  server.off("error", onError);
5997
- resolve16();
6846
+ resolve18();
5998
6847
  };
5999
6848
  server.once("error", onError);
6000
6849
  server.once("listening", onListening);
@@ -6002,7 +6851,7 @@ async function createAuthenticatedLoopbackService(options) {
6002
6851
  });
6003
6852
  const address = server.address();
6004
6853
  if (address === null || typeof address === "string") {
6005
- await new Promise((resolve16) => server.close(() => resolve16()));
6854
+ await new Promise((resolve18) => server.close(() => resolve18()));
6006
6855
  throw new Error("loopback service did not expose a TCP address");
6007
6856
  }
6008
6857
  const endpoint = `http://${host}:${address.port}/run`;
@@ -6017,14 +6866,17 @@ async function createAuthenticatedLoopbackService(options) {
6017
6866
  async close() {
6018
6867
  if (closed) return;
6019
6868
  closed = true;
6020
- await new Promise((resolve16, reject) => {
6021
- server.close((error) => error === void 0 ? resolve16() : reject(error));
6869
+ await new Promise((resolve18, reject) => {
6870
+ server.close((error) => error === void 0 ? resolve18() : reject(error));
6022
6871
  });
6023
6872
  transport.close();
6024
6873
  }
6025
6874
  };
6026
6875
  }
6027
6876
 
6028
- export { DEFAULT_ADMISSION_THRESHOLDS, RHI_DEBUG_OPERATION_MANIFEST, analyzePreviewArtifacts, assetAddCommand, assetInspectCommand, assetListCommand, assetVerifyCommand, bootstrapRealm, buildCommand, createAdmissionReport, createAuthenticatedLoopbackService, createAuthorContribution, createBuildContribution, createCarrierProvider, createCarrierProviderService, createCarrierRendezvous, createCliRhiDebugOperationContext, createDefaultContributions, createDevkitToolRuntime, createDomainPreviewContributions, createInitPlan, createMigrationRoster, createOfflineAnalysisContribution, createPreviewContribution, createPreviewContributions, createPreviewToolRuntime, createResourceProbe, createRhiDebugOperationContext, createServiceCache, createServiceExecutor, createToolClient, describeTool, devCommand, discoverRhiDebugOperations, doctorCommand, initCommand, listTools, materializeToolCatalog, newCommand, packageCommand, pluginInstallCommand, pluginUninstallCommand, previewCommand, readProjectFacts, rebuildToolCatalog, recoverRhiDebugError, renderRhiDebugHelp, resolveMigration, resolveRealmCapability, runBenchmarkAdmission, runCarrierPreviewRoute, runGenericTool, runLibraryTool, runNamedTool, runPreviewHost, runPrivateTool, runRhiDebugCommand, runRhiDebugOperation, sdkInstallCommand, shaderCheckCommand, skillInstallCommand, skillVerifyCommand, summarizeBenchmarkSamples, testCommand, verifyDist, writeDistManifest };
6877
+ // src/index.ts
6878
+ init_types();
6879
+
6880
+ export { DEFAULT_ADMISSION_THRESHOLDS, ENGINE_BINDING_SCHEMA_VERSION, RHI_DEBUG_OPERATION_MANIFEST, analyzePreviewArtifacts, assetAddCommand, assetInspectCommand, assetListCommand, assetVerifyCommand, bootstrapRealm, browserCaptureCommand, buildCommand, createAdmissionReport, createAuthenticatedLoopbackService, createAuthorContribution, createBrowserCapture, createBuildContribution, createCarrierProvider, createCarrierProviderService, createCarrierRendezvous, createCliRhiDebugOperationContext, createDefaultContributions, createDevkitToolRuntime, createDomainPreviewContributions, createInitPlan, createMigrationRoster, createOfflineAnalysisContribution, createPreviewContribution, createPreviewContributions, createPreviewToolRuntime, createResourceProbe, createRhiDebugOperationContext, createServiceCache, createServiceExecutor, createSoftwareBrowser, createToolClient, describeTool, devCommand, discoverRhiDebugOperations, doctorCommand, engineBindingFilePath, engineDoctorCommand, engineStatusCommand, engineUnlinkCommand, engineUseLocalCommand, initCommand, inspectEngineWorkspace, listTools, materializeToolCatalog, newCommand, packageCommand, pluginInstallCommand, pluginUninstallCommand, previewCommand, readEngineBinding, readProjectFacts, rebuildToolCatalog, recoverRhiDebugError, renderRhiDebugHelp, resolveMigration, resolveProjectPort, resolveRealmCapability, runBenchmarkAdmission, runCarrierPreviewRoute, runGenericTool, runLibraryTool, runNamedTool, runPreviewHost, runPrivateTool, runRhiDebugCommand, runRhiDebugOperation, sdkInstallCommand, shaderCheckCommand, skillInstallCommand, skillVerifyCommand, softwareCaptureCommand, summarizeBenchmarkSamples, testCommand, verifyDist, writeDistManifest };
6029
6881
  //# sourceMappingURL=index.mjs.map
6030
6882
  //# sourceMappingURL=index.mjs.map