@forgeax/engine-devkit 0.1.6 → 0.1.19

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 (57) hide show
  1. package/README.md +155 -2
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/__tests__/engine-binding.unit.test.d.ts +2 -0
  4. package/dist/__tests__/engine-binding.unit.test.d.ts.map +1 -0
  5. package/dist/__tests__/native-preview-dispatch.test.d.ts +2 -0
  6. package/dist/__tests__/native-preview-dispatch.test.d.ts.map +1 -0
  7. package/dist/__tests__/native-preview-subject-draw.unit.test.d.ts +2 -0
  8. package/dist/__tests__/native-preview-subject-draw.unit.test.d.ts.map +1 -0
  9. package/dist/__tests__/single-html-runtime.e2e.test.d.ts +2 -0
  10. package/dist/__tests__/single-html-runtime.e2e.test.d.ts.map +1 -0
  11. package/dist/__tests__/single-html.test.d.ts +2 -0
  12. package/dist/__tests__/single-html.test.d.ts.map +1 -0
  13. package/dist/__tests__/software-browser-session.unit.test.d.ts +2 -0
  14. package/dist/__tests__/software-browser-session.unit.test.d.ts.map +1 -0
  15. package/dist/__tests__/software-capture-pixels.unit.test.d.ts +2 -0
  16. package/dist/__tests__/software-capture-pixels.unit.test.d.ts.map +1 -0
  17. package/dist/__tests__/software-capture.unit.test.d.ts +2 -0
  18. package/dist/__tests__/software-capture.unit.test.d.ts.map +1 -0
  19. package/dist/bootstrap-commands.d.ts.map +1 -1
  20. package/dist/cli-output.d.ts.map +1 -1
  21. package/dist/cli.mjs +3350 -475
  22. package/dist/cli.mjs.map +1 -1
  23. package/dist/commands.d.ts +2 -0
  24. package/dist/commands.d.ts.map +1 -1
  25. package/dist/dist.d.ts +2 -2
  26. package/dist/dist.d.ts.map +1 -1
  27. package/dist/engine-binding.d.ts +58 -0
  28. package/dist/engine-binding.d.ts.map +1 -0
  29. package/dist/host.d.ts +6 -1
  30. package/dist/host.d.ts.map +1 -1
  31. package/dist/index.d.ts +10 -3
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.mjs +3679 -1368
  34. package/dist/index.mjs.map +1 -1
  35. package/dist/rhi-debug/cli-context.d.ts.map +1 -1
  36. package/dist/rhi-debug/operations.d.ts +3 -3
  37. package/dist/rhi-debug/operations.d.ts.map +1 -1
  38. package/dist/sdk-bootstrap.d.ts +6 -1
  39. package/dist/sdk-bootstrap.d.ts.map +1 -1
  40. package/dist/sdk-cli.mjs +35 -15
  41. package/dist/sdk-cli.mjs.map +1 -1
  42. package/dist/sdk.d.ts +1 -3
  43. package/dist/sdk.d.ts.map +1 -1
  44. package/dist/single-html.d.ts +43 -0
  45. package/dist/single-html.d.ts.map +1 -0
  46. package/dist/software-capture.d.ts +161 -0
  47. package/dist/software-capture.d.ts.map +1 -0
  48. package/dist/tools/browser-host.d.ts.map +1 -1
  49. package/dist/tools/client.d.ts.map +1 -1
  50. package/dist/tools/commands.d.ts +5 -0
  51. package/dist/tools/commands.d.ts.map +1 -1
  52. package/dist/tools/native-preview.d.ts.map +1 -1
  53. package/dist/tools/preview-contributions.d.ts +0 -5
  54. package/dist/tools/preview-contributions.d.ts.map +1 -1
  55. package/dist/types.d.ts +23 -1
  56. package/dist/types.d.ts.map +1 -1
  57. package/package.json +34 -30
package/dist/cli.mjs CHANGED
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { deflateRawSync } from 'zlib';
3
- import { createHash } from 'crypto';
4
- import { readFile, mkdtemp, rm, readdir, mkdir, cp, rename, writeFile, copyFile, stat, symlink, unlink, realpath, readlink, lstat, rmdir, access } from 'fs/promises';
5
- import { resolve, extname, dirname, basename, relative, isAbsolute, sep } from 'path';
3
+ import { createHash, randomUUID } from 'crypto';
4
+ import { readFile, mkdtemp, rm, readdir, mkdir, cp, rename, writeFile, realpath, copyFile, stat, symlink, unlink, access, readlink, lstat, rmdir } from 'fs/promises';
5
+ import { resolve, extname, dirname, basename, relative, isAbsolute, sep, join } from 'path';
6
+ import { GameProjectSchema } from '@forgeax/engine-project';
6
7
  import { readFileSync, existsSync } from 'fs';
7
8
  import { createRequire } from 'module';
8
9
  import { pathToFileURL, fileURLToPath } from 'url';
@@ -13,31 +14,35 @@ import { gltfImporter } from '@forgeax/engine-gltf';
13
14
  import { imageImporter } from '@forgeax/engine-image/image-importer';
14
15
  import { BUILTIN_MESH_ASSETS } from '@forgeax/engine-pack/builtin';
15
16
  import { scanInventory } from '@forgeax/engine-pack/scanner';
16
- import { describeResourcePreviewFailure, validateCanonicalKitReceipt, createMaterialPreviewContribution, createMeshPreviewContribution, createVfxPreviewContribution, createTexturePreviewContribution, validatePreviewArtifactManifest } from '@forgeax/engine-preview';
17
+ import { describeResourcePreviewFailure, validateCanonicalKitReceipt, RESOURCE_PREVIEW_DEFAULT_SIZE, createNativePreviewHost, bindPreviewHost, createResourcePreviewReport, validatePreviewArtifactManifest as validatePreviewArtifactManifest$1 } from '@forgeax/engine-preview';
17
18
  import { createMaterialPackCooker } from '@forgeax/engine-shader-compiler';
18
19
  import { createStandaloneRuntimeAssetBinding, ok, err } from '@forgeax/engine-types';
19
20
  import { createParticleCodeNativeCookerFromRoots } from '@forgeax/engine-vfx-compiler';
20
21
  import { pluginPack, reloadAssetHost } from '@forgeax/engine-vite-plugin-pack';
22
+ import { vitePluginRhiDebug } from '@forgeax/engine-vite-plugin-rhi-debug';
21
23
  import { forgeaxShader } from '@forgeax/engine-vite-plugin-shader';
22
- import { GameProjectSchema } from '@forgeax/engine-project';
24
+ import { tmpdir } from 'os';
25
+ import { parse } from 'parse5';
26
+ import { build, preview, parseAst, Visitor, createServer as createServer$1 } from 'vite';
23
27
  import { runCliGltf } from '@forgeax/engine-gltf/cli-gltf';
24
28
  import { scanEntries } from '@forgeax/engine-pack/cli-asset';
25
29
  import { AssetGuid } from '@forgeax/engine-pack/guid';
26
- import { execFile } from 'child_process';
27
- import { tmpdir } from 'os';
30
+ import { spawn, execFile } from 'child_process';
28
31
  import { promisify } from 'util';
29
- import { decodeTape, openReplay, buildFrameModel, createRhiDebugError } from '@forgeax/engine-rhi-debug';
30
- import { rhi, createShaderModule } from '@forgeax/engine-rhi-null';
31
- import { build, preview, createServer as createServer$1 } from 'vite';
32
+ import { decodeTape, openReplay, buildFrameModel, replayDeviceRequest, createRhiDebugError } from '@forgeax/engine-rhi-debug';
33
+ import { rhi, createShaderModule } from '@forgeax/engine-rhi-webgpu';
32
34
  import { createServer } from 'net';
35
+ import { FORGEAX_FRAME_SUBMITTED_DATASET, createToolPreviewRecipe } from '@forgeax/engine-app';
36
+ import { parseImage } from '@forgeax/engine-image/parse-image';
37
+ import { chromium } from 'playwright';
33
38
  import { startVitest } from 'vitest/node';
34
39
  import materialPreviewPlugin from '@forgeax/engine-preview/material';
35
40
  import meshPreviewPlugin from '@forgeax/engine-preview/mesh';
36
41
  import texturePreviewPlugin from '@forgeax/engine-preview/texture';
37
42
  import vfxPreviewPlugin from '@forgeax/engine-preview/vfx';
38
- import { isSerializableValue, domainFailureError, createToolRuntime, capabilityUnavailableError, defineTool, validateArtifactManifest } from '@forgeax/engine-tool-runtime';
43
+ import { isSerializableValue, domainFailureError, createToolRuntime, capabilityUnavailableError, defineTool, createPreviewArtifactManifest, validatePreviewArtifactManifest, validateArtifactManifest } from '@forgeax/engine-tool-runtime';
39
44
  import { Context, isToolPlugin } from '@forgeax/engine-plugin';
40
- import { installCatalogLoader, projectPluginEntries } from '@forgeax/engine-plugin/loader';
45
+ import { installCatalogLoader, projectPluginEntries, createContextCapabilityResolver } from '@forgeax/engine-plugin/loader';
41
46
 
42
47
  var __defProp = Object.defineProperty;
43
48
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -145,6 +150,20 @@ function mediaType(path) {
145
150
  if (path.endsWith(".wasm")) return "application/wasm";
146
151
  if (path.endsWith(".png")) return "image/png";
147
152
  if (path.endsWith(".jpg") || path.endsWith(".jpeg")) return "image/jpeg";
153
+ if (path.endsWith(".css")) return "text/css";
154
+ if (path.endsWith(".svg")) return "image/svg+xml";
155
+ if (path.endsWith(".webp")) return "image/webp";
156
+ if (path.endsWith(".gif")) return "image/gif";
157
+ if (path.endsWith(".mp3")) return "audio/mpeg";
158
+ if (path.endsWith(".wav")) return "audio/wav";
159
+ if (path.endsWith(".ogg")) return "audio/ogg";
160
+ if (path.endsWith(".mp4")) return "video/mp4";
161
+ if (path.endsWith(".webm")) return "video/webm";
162
+ if (path.endsWith(".woff")) return "font/woff";
163
+ if (path.endsWith(".woff2")) return "font/woff2";
164
+ if (path.endsWith(".ttf")) return "font/ttf";
165
+ if (path.endsWith(".otf")) return "font/otf";
166
+ if (path.endsWith(".wgsl") || path.endsWith(".glsl")) return "text/plain";
148
167
  return "application/octet-stream";
149
168
  }
150
169
  async function filesUnder(root, directory = root) {
@@ -301,146 +320,668 @@ var init_dist = __esm({
301
320
  "src/dist.ts"() {
302
321
  }
303
322
  });
304
-
305
- // src/host.ts
306
- var host_exports = {};
307
- __export(host_exports, {
308
- createViteConfig: () => createViteConfig,
309
- devKitDdcRoots: () => devKitDdcRoots,
310
- ignoreDevKitCatalogPath: () => ignoreDevKitCatalogPath
311
- });
312
- function ignoreDevKitCatalogPath(path) {
313
- const normalized = path.replace(/\\/g, "/");
314
- return normalized.split("/").includes("shaders");
323
+ function projectError(code, expected, hint, detail) {
324
+ return { ok: false, error: { code, expected, hint, detail } };
315
325
  }
316
- function devKitDdcRoots(projectRoot2) {
317
- return {
318
- buildCacheRoot: resolve(projectRoot2, ".forgeax", "ddc", "build-cache"),
319
- projectDdcRoot: resolve(projectRoot2, ".forgeax", "ddc", "v2")
320
- };
326
+ async function readJson(path) {
327
+ return JSON.parse(await readFile(path, "utf8"));
321
328
  }
322
- function isResourcePreviewIgnoredPath(path) {
323
- return ignoreDevKitCatalogPath(path) || path.endsWith(".wgsl.meta.json") || path.endsWith("target-profile.json.meta.json");
329
+ function firstUnsupportedStandaloneRealm(entries2, inheritedRealm = "engine") {
330
+ for (const entry of entries2) {
331
+ const realm = entry.realm ?? inheritedRealm ?? "engine";
332
+ if (realm !== "engine") return { id: entry.id, realm };
333
+ if (entry.group === true) {
334
+ const unsupported = firstUnsupportedStandaloneRealm(
335
+ entry.config,
336
+ realm
337
+ );
338
+ if (unsupported !== void 0) return unsupported;
339
+ }
340
+ }
341
+ return void 0;
324
342
  }
325
- function isProjectSourceIgnoredPath(path) {
326
- return ignoreDevKitCatalogPath(path) || path.endsWith(".wgsl.meta.json");
343
+ function pluginModuleNames(entries2) {
344
+ return entries2.flatMap(
345
+ (entry) => entry.group === true ? pluginModuleNames(entry.config) : [entry.name]
346
+ );
327
347
  }
328
- async function prepareBuiltinPack(projectRoots, generated, ignorePath) {
329
- const inventory = await scanInventory(projectRoots, { ignorePath });
330
- if (!inventory.ok) return void 0;
331
- const declared = /* @__PURE__ */ new Set();
332
- for (const declaration of inventory.value.declarations.values()) {
333
- if (declaration.format === "pack.json") {
334
- for (const asset of declaration.value.assets) declared.add(asset.guid.toLowerCase());
335
- continue;
348
+ async function readProjectFacts(rootInput = process.cwd()) {
349
+ const root = resolve(rootInput);
350
+ let forgeValue;
351
+ let packageValue;
352
+ try {
353
+ [forgeValue, packageValue] = await Promise.all([
354
+ readJson(resolve(root, "forge.json")),
355
+ readJson(resolve(root, "package.json"))
356
+ ]);
357
+ } catch (cause) {
358
+ return projectError(
359
+ "project-manifest-unreadable",
360
+ "readable forge.json and package.json files",
361
+ "Run the command from a ForgeaX game root or pass its directory.",
362
+ { root, reason: cause instanceof Error ? cause.message : String(cause) }
363
+ );
364
+ }
365
+ if (forgeValue === null || typeof forgeValue !== "object") {
366
+ return projectError(
367
+ "project-manifest-invalid",
368
+ "forge.json to contain an object",
369
+ "Repair forge.json before running DevKit.",
370
+ { root }
371
+ );
372
+ }
373
+ if (packageValue === null || typeof packageValue !== "object") {
374
+ return projectError(
375
+ "package-manifest-invalid",
376
+ "package.json to contain an object",
377
+ "Repair package.json before running DevKit.",
378
+ { root }
379
+ );
380
+ }
381
+ const parsedForge = GameProjectSchema.safeParse(forgeValue);
382
+ if (!parsedForge.success) {
383
+ return projectError(
384
+ "project-manifest-invalid",
385
+ "forge.json to satisfy @forgeax/engine-project GameProjectSchema",
386
+ "Repair the fields reported by the authoritative project schema.",
387
+ { root, issues: parsedForge.error.issues }
388
+ );
389
+ }
390
+ const forge = parsedForge.data;
391
+ if (forge.id.length === 0 || forge.name.length === 0 || forge.entry === void 0 || forge.entry.length === 0) {
392
+ return projectError(
393
+ "project-manifest-invalid",
394
+ "forge.json to declare id, name, and entry",
395
+ "Restore the project entry; plugin Entries remain optional additions.",
396
+ { root }
397
+ );
398
+ }
399
+ const plugins = forge.plugins ?? [];
400
+ const unsupportedRealm = firstUnsupportedStandaloneRealm(plugins);
401
+ if (unsupportedRealm !== void 0) {
402
+ return projectError(
403
+ "project-plugin-realm-unsupported",
404
+ "the standalone Devkit host to contain only engine-realm plugin Entries",
405
+ "Move Host or build plugins to a host that owns that physical realm.",
406
+ { root, ...unsupportedRealm }
407
+ );
408
+ }
409
+ const entryPath = isAbsolute(forge.entry) ? forge.entry : resolve(root, forge.entry);
410
+ try {
411
+ await readFile(entryPath);
412
+ } catch {
413
+ return projectError(
414
+ "project-entry-missing",
415
+ "forge.json#entry to resolve to a readable module",
416
+ "Restore the game entry or update forge.json#entry.",
417
+ { root, entry: forge.entry }
418
+ );
419
+ }
420
+ const packageJson = packageValue;
421
+ const forgeax = packageJson.forgeax;
422
+ const configuredRoots = forgeax !== null && typeof forgeax === "object" ? forgeax.assets?.roots : void 0;
423
+ const assetRoots = Array.isArray(configuredRoots) && configuredRoots.every((value) => typeof value === "string") ? configuredRoots : ["assets"];
424
+ const configuredImporters = forgeax !== null && typeof forgeax === "object" ? forgeax.assets?.importers : void 0;
425
+ const assetImporters = Array.isArray(configuredImporters) && configuredImporters.every((value) => typeof value === "string") ? configuredImporters : [];
426
+ const configuredPublicDir = forgeax !== null && typeof forgeax === "object" ? forgeax.assets?.publicDir : void 0;
427
+ const assetPublicDir = typeof configuredPublicDir === "string" ? configuredPublicDir : void 0;
428
+ const physics = forge.physics === "2d" || forge.physics === "3d" ? forge.physics : void 0;
429
+ const defaultScene = typeof forge.defaultScene === "string" && forge.defaultScene.length > 0 ? forge.defaultScene : void 0;
430
+ const normalizedEntry = forge.entry.startsWith("./") ? forge.entry : `./${forge.entry}`;
431
+ const bootstrapEntry = pluginModuleNames(plugins).map((name) => name.startsWith("./") ? name : `./${name}`).includes(normalizedEntry) ? void 0 : forge.entry;
432
+ return {
433
+ ok: true,
434
+ value: {
435
+ root,
436
+ id: forge.id,
437
+ name: forge.name,
438
+ entry: forge.entry,
439
+ ...bootstrapEntry === void 0 ? {} : { bootstrapEntry },
440
+ plugins,
441
+ ...physics === void 0 ? {} : { physics },
442
+ ...defaultScene === void 0 ? {} : { defaultScene },
443
+ assetRoots,
444
+ ...assetImporters.length === 0 ? {} : { assetImporters },
445
+ ...assetPublicDir === void 0 ? {} : { assetPublicDir },
446
+ packageJson
336
447
  }
337
- for (const asset of declaration.value.subAssets) declared.add(asset.guid.toLowerCase());
448
+ };
449
+ }
450
+ function commandError(cause, fallbackCode) {
451
+ if (cause !== null && typeof cause === "object" && "code" in cause && "expected" in cause && "hint" in cause && "detail" in cause) {
452
+ return cause;
338
453
  }
339
- const missing = BUILTIN_MESH_ASSETS.filter((asset) => !declared.has(asset.guid.toLowerCase()));
340
- if (missing.length === 0) return void 0;
341
- const packPath = resolve(generated, "engine-builtins.pack.json");
342
- await writeFile(
343
- packPath,
344
- `${JSON.stringify(
345
- {
346
- schemaVersion: "2.0.0",
347
- kind: "internal-text-package",
348
- assets: missing.map((asset) => ({
349
- guid: asset.guid,
350
- kind: "mesh",
351
- payload: { geometry: asset.geometry },
352
- refs: [],
353
- artifacts: {}
354
- }))
355
- },
356
- null,
357
- 2
358
- )}
359
- `,
360
- "utf8"
361
- );
362
- return packPath;
454
+ return {
455
+ code: fallbackCode,
456
+ expected: "the ForgeaX command to complete",
457
+ hint: "Inspect the underlying diagnostic and repair the owning input.",
458
+ detail: { reason: cause instanceof Error ? cause.message : String(cause) }
459
+ };
363
460
  }
364
- function findEngineWorkspaceRoot() {
365
- let cursor = dirname(fileURLToPath(import.meta.url));
366
- for (; ; ) {
367
- if (existsSync(resolve(cursor, "pnpm-workspace.yaml"))) return cursor;
368
- const parent = dirname(cursor);
369
- if (parent === cursor) return void 0;
370
- cursor = parent;
461
+ var init_project = __esm({
462
+ "src/project.ts"() {
371
463
  }
464
+ });
465
+ function bindingError(code, expected, hint, detail = {}) {
466
+ return { ok: false, error: { code, expected, hint, detail } };
372
467
  }
373
- async function engineWorkspacePackages() {
374
- if (engineWorkspacePackagesPromise !== void 0) return engineWorkspacePackagesPromise;
375
- engineWorkspacePackagesPromise = (async () => {
376
- const workspaceRoot = findEngineWorkspaceRoot();
377
- if (workspaceRoot === void 0) return /* @__PURE__ */ new Map();
378
- const packageRoot = resolve(workspaceRoot, "packages");
379
- if (!existsSync(packageRoot)) return /* @__PURE__ */ new Map();
380
- const packages = /* @__PURE__ */ new Map();
381
- for (const entry of await readdir(packageRoot, { withFileTypes: true })) {
382
- if (!entry.isDirectory()) continue;
383
- const root = resolve(packageRoot, entry.name);
384
- try {
385
- const manifest = JSON.parse(
386
- await readFile(resolve(root, "package.json"), "utf8")
387
- );
388
- if (manifest === null || typeof manifest !== "object") continue;
389
- const name = manifest.name;
390
- if (typeof name === "string" && name.startsWith("@forgeax/engine-")) {
391
- packages.set(name, { root, manifest });
392
- }
393
- } catch {
394
- }
468
+ function isMissing(cause) {
469
+ return cause !== null && typeof cause === "object" && "code" in cause && cause.code === "ENOENT";
470
+ }
471
+ async function pathExists(path) {
472
+ try {
473
+ await access(path);
474
+ return true;
475
+ } catch {
476
+ return false;
477
+ }
478
+ }
479
+ function parseBinding(value, path) {
480
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
481
+ return bindingError(
482
+ "engine-binding-invalid",
483
+ "engine-binding.json to contain one object",
484
+ "Run forgeax engine unlink, then select a binding again.",
485
+ { path }
486
+ );
487
+ }
488
+ const candidate = value;
489
+ if (candidate.schemaVersion !== ENGINE_BINDING_SCHEMA_VERSION) {
490
+ return bindingError(
491
+ "engine-binding-version-unsupported",
492
+ `engine-binding.json schemaVersion ${ENGINE_BINDING_SCHEMA_VERSION}`,
493
+ "Upgrade the SDK or remove the stale binding with forgeax engine unlink.",
494
+ { path, schemaVersion: candidate.schemaVersion ?? null }
495
+ );
496
+ }
497
+ if (typeof candidate.path !== "string" || candidate.path.length === 0) {
498
+ return bindingError(
499
+ "engine-binding-path-missing",
500
+ "engine-binding.json to contain one non-empty local Engine path",
501
+ "Run forgeax engine use-local <engine-directory>.",
502
+ { path }
503
+ );
504
+ }
505
+ return {
506
+ ok: true,
507
+ value: {
508
+ schemaVersion: ENGINE_BINDING_SCHEMA_VERSION,
509
+ path: resolve(candidate.path)
395
510
  }
396
- return packages;
397
- })();
398
- return engineWorkspacePackagesPromise;
511
+ };
512
+ }
513
+ async function readEngineBinding(rootInput = process.cwd()) {
514
+ const root = resolve(rootInput);
515
+ const path = bindingFile(root);
516
+ try {
517
+ const value = JSON.parse(await readFile(path, "utf8"));
518
+ return parseBinding(value, path);
519
+ } catch (cause) {
520
+ if (isMissing(cause)) return { ok: true, value: null };
521
+ return bindingError(
522
+ "engine-binding-unreadable",
523
+ "engine-binding.json to be readable JSON",
524
+ "Repair or remove .forgeax/engine-binding.json, then select a binding again.",
525
+ { path, reason: cause instanceof Error ? cause.message : String(cause) }
526
+ );
527
+ }
528
+ }
529
+ async function writeEngineBinding(root, binding) {
530
+ const path = bindingFile(root);
531
+ const partial = `${path}.partial-${process.pid}`;
532
+ await mkdir(dirname(path), { recursive: true });
533
+ await writeFile(partial, `${JSON.stringify(binding, null, 2)}
534
+ `, "utf8");
535
+ await rename(partial, path);
399
536
  }
400
537
  function conditionalExport(value) {
401
538
  if (typeof value === "string") return value;
402
- if (value === null || Array.isArray(value)) return void 0;
403
- for (const condition of ["browser", "import", "node", "default"]) {
404
- const candidate = value[condition];
405
- if (candidate === void 0) continue;
539
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
540
+ const record = value;
541
+ for (const key of ["import", "browser", "node", "default", "require", "types"]) {
542
+ const candidate = record[key];
406
543
  const selected = conditionalExport(candidate);
407
544
  if (selected !== void 0) return selected;
408
545
  }
409
546
  return void 0;
410
547
  }
411
- function packageExportTarget(manifest, subpath) {
548
+ function packageEntry(manifest) {
412
549
  const exportsValue = manifest.exports;
413
- if (exportsValue === void 0) {
414
- if (subpath.length > 0) return void 0;
415
- const main = manifest.module ?? manifest.main;
416
- return typeof main === "string" ? main : void 0;
550
+ if (exportsValue !== void 0) {
551
+ if (typeof exportsValue === "object" && exportsValue !== null && !Array.isArray(exportsValue)) {
552
+ const root = exportsValue["."];
553
+ const selected2 = conditionalExport(root ?? exportsValue);
554
+ if (selected2 !== void 0) return selected2;
555
+ }
556
+ const selected = conditionalExport(exportsValue);
557
+ if (selected !== void 0) return selected;
417
558
  }
418
- if (typeof exportsValue === "string" || exportsValue === null) {
419
- return subpath.length === 0 ? conditionalExport(exportsValue) : void 0;
559
+ return typeof manifest.module === "string" ? manifest.module : typeof manifest.main === "string" ? manifest.main : void 0;
560
+ }
561
+ async function readManifest(path) {
562
+ const value = JSON.parse(await readFile(path, "utf8"));
563
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
564
+ throw new Error(`engine-package-manifest-invalid: ${path}`);
420
565
  }
421
- const keys = Object.keys(exportsValue);
422
- const subpathMap = keys.some((key) => key === "." || key.startsWith("./"));
423
- if (!subpathMap) return subpath.length === 0 ? conditionalExport(exportsValue) : void 0;
424
- const requested = subpath.length === 0 ? "." : `./${subpath}`;
425
- const exact = exportsValue[requested];
426
- if (exact !== void 0) return conditionalExport(exact);
427
- for (const key of keys) {
428
- const marker = key.indexOf("*");
429
- if (marker < 0) continue;
430
- const prefix = key.slice(0, marker);
431
- const suffix = key.slice(marker + 1);
566
+ return value;
567
+ }
568
+ async function inspectEngineWorkspace(workspaceInput) {
569
+ const root = resolve(workspaceInput);
570
+ const packageRoot = resolve(root, "packages");
571
+ try {
572
+ await access(resolve(root, "pnpm-workspace.yaml"));
573
+ const entries2 = await readdir(packageRoot, { withFileTypes: true });
574
+ const packages = [];
575
+ for (const entry of entries2) {
576
+ if (!entry.isDirectory()) continue;
577
+ const packagePath = resolve(packageRoot, entry.name);
578
+ let manifest;
579
+ try {
580
+ manifest = await readManifest(resolve(packagePath, "package.json"));
581
+ } catch {
582
+ continue;
583
+ }
584
+ if (typeof manifest.name !== "string" || !manifest.name.startsWith("@forgeax/engine"))
585
+ continue;
586
+ if (typeof manifest.version !== "string") {
587
+ return bindingError(
588
+ "engine-package-version-missing",
589
+ "every local Engine package to declare a version",
590
+ "Restore the package manifest version before selecting a local Engine.",
591
+ { package: manifest.name, root: packagePath }
592
+ );
593
+ }
594
+ const target = packageEntry(manifest);
595
+ const entryPath = target === void 0 ? void 0 : resolve(packagePath, target);
596
+ let entryDigest = null;
597
+ let builtAt2 = null;
598
+ if (entryPath !== void 0 && await pathExists(entryPath)) {
599
+ const [bytes, metadata] = await Promise.all([readFile(entryPath), stat(entryPath)]);
600
+ entryDigest = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
601
+ builtAt2 = metadata.mtime.toISOString();
602
+ }
603
+ packages.push({
604
+ name: manifest.name,
605
+ root: packagePath,
606
+ version: manifest.version,
607
+ entry: entryPath ?? null,
608
+ built: entryDigest !== null,
609
+ entryDigest,
610
+ builtAt: builtAt2
611
+ });
612
+ }
613
+ packages.sort((a, b) => a.name.localeCompare(b.name));
614
+ if (packages.length === 0) {
615
+ return bindingError(
616
+ "engine-workspace-empty",
617
+ "the local Engine workspace to contain @forgeax/engine packages",
618
+ "Pass the Engine repository or its source/engine SDK directory.",
619
+ { root, packageRoot }
620
+ );
621
+ }
622
+ const versions = [...new Set(packages.map((item) => item.version))].sort();
623
+ const missingBuilds = packages.filter((item) => !item.built).map((item) => item.name);
624
+ const builtAt = packages.flatMap((item) => item.builtAt === null ? [] : [item.builtAt]).sort().at(-1) ?? null;
625
+ const digest = `sha256:${createHash("sha256").update(
626
+ packages.map((item) => `${item.name}\0${item.version}\0${item.entryDigest ?? "unbuilt"}`).join("\n")
627
+ ).digest("hex")}`;
628
+ return {
629
+ ok: true,
630
+ value: {
631
+ root,
632
+ packageCount: packages.length,
633
+ builtPackages: packages.length - missingBuilds.length,
634
+ missingBuilds,
635
+ versions,
636
+ packages,
637
+ digest,
638
+ builtAt
639
+ }
640
+ };
641
+ } catch (cause) {
642
+ return bindingError(
643
+ isMissing(cause) ? "engine-workspace-missing" : "engine-workspace-unreadable",
644
+ "a readable Engine workspace with pnpm-workspace.yaml and packages/",
645
+ "Pass the Engine repository root, not its packages/ directory.",
646
+ { root, packageRoot, reason: cause instanceof Error ? cause.message : String(cause) }
647
+ );
648
+ }
649
+ }
650
+ function projectDependencyMode(packageJson) {
651
+ let workspace = false;
652
+ let registry = false;
653
+ for (const section of [
654
+ "dependencies",
655
+ "devDependencies",
656
+ "optionalDependencies",
657
+ "peerDependencies"
658
+ ]) {
659
+ const value = packageJson[section];
660
+ if (value === null || typeof value !== "object" || Array.isArray(value)) continue;
661
+ for (const version of Object.values(value)) {
662
+ if (typeof version !== "string") continue;
663
+ if (version.startsWith("workspace:") || version.startsWith("file:")) workspace = true;
664
+ else registry = true;
665
+ }
666
+ }
667
+ return workspace ? registry ? "mixed" : "workspace" : registry ? "registry" : "none";
668
+ }
669
+ async function sdkResolution(root) {
670
+ const packageJson = resolve(root, "node_modules", "@forgeax", "engine", "package.json");
671
+ try {
672
+ const manifest = await readManifest(packageJson);
673
+ const target = packageEntry(manifest);
674
+ const entry = target === void 0 ? null : resolve(dirname(packageJson), target);
675
+ return {
676
+ root: dirname(packageJson),
677
+ version: typeof manifest.version === "string" ? manifest.version : null,
678
+ entry,
679
+ built: entry !== null && await pathExists(entry),
680
+ source: "sdk"
681
+ };
682
+ } catch {
683
+ return { root: null, version: null, entry: null, built: false, source: "unresolved" };
684
+ }
685
+ }
686
+ async function engineStatusCommand(options = {}) {
687
+ const facts = await readProjectFacts(options.root);
688
+ if (!facts.ok) return facts;
689
+ const bindingResult = await readEngineBinding(facts.value.root);
690
+ if (!bindingResult.ok) return bindingResult;
691
+ const binding = bindingResult.value;
692
+ const mode = binding === null ? "sdk" : "local";
693
+ const projectDependencies = projectDependencyMode(facts.value.packageJson);
694
+ if (binding !== null) {
695
+ const localPath = binding.path;
696
+ const localBinding = {
697
+ schemaVersion: ENGINE_BINDING_SCHEMA_VERSION,
698
+ path: localPath
699
+ };
700
+ const workspaceResult = await inspectEngineWorkspace(localPath);
701
+ if (!workspaceResult.ok) {
702
+ const resolved3 = {
703
+ root: localPath,
704
+ version: null,
705
+ entry: null,
706
+ built: false,
707
+ source: "local"
708
+ };
709
+ return {
710
+ ok: true,
711
+ value: {
712
+ schemaVersion: ENGINE_BINDING_SCHEMA_VERSION,
713
+ root: facts.value.root,
714
+ binding: localBinding,
715
+ mode,
716
+ resolved: resolved3,
717
+ workspace: null,
718
+ projectDependencies,
719
+ npmCompatible: projectDependencies === "registry" || projectDependencies === "none",
720
+ healthy: false,
721
+ next: ["forgeax engine unlink", "forgeax engine use-local <engine-directory>"],
722
+ diagnostic: workspaceResult.error
723
+ }
724
+ };
725
+ }
726
+ const workspace = workspaceResult.value;
727
+ const version = workspace.versions.length === 1 ? workspace.versions[0] ?? null : null;
728
+ const umbrella = workspace.packages.find((item) => item.name === "@forgeax/engine");
729
+ const resolved2 = {
730
+ root: localPath,
731
+ version,
732
+ entry: umbrella?.entry ?? null,
733
+ built: umbrella?.built ?? false,
734
+ source: "local"
735
+ };
736
+ const healthy = workspace.versions.length === 1 && workspace.missingBuilds.length === 0;
737
+ return {
738
+ ok: true,
739
+ value: {
740
+ schemaVersion: ENGINE_BINDING_SCHEMA_VERSION,
741
+ root: facts.value.root,
742
+ binding: localBinding,
743
+ mode,
744
+ resolved: resolved2,
745
+ workspace,
746
+ projectDependencies,
747
+ npmCompatible: projectDependencies === "registry" || projectDependencies === "none",
748
+ healthy,
749
+ next: healthy ? ["forgeax build", "forgeax capture --backend auto --require-ui"] : ["pnpm build:engine", "forgeax engine doctor"]
750
+ }
751
+ };
752
+ }
753
+ const resolved = await sdkResolution(facts.value.root);
754
+ return {
755
+ ok: true,
756
+ value: {
757
+ schemaVersion: ENGINE_BINDING_SCHEMA_VERSION,
758
+ root: facts.value.root,
759
+ binding,
760
+ mode,
761
+ resolved,
762
+ workspace: null,
763
+ projectDependencies,
764
+ npmCompatible: projectDependencies === "registry" || projectDependencies === "none",
765
+ healthy: resolved.source === "sdk" && resolved.built,
766
+ next: resolved.source === "sdk" && resolved.built ? ["forgeax build", "forgeax capture --backend auto --require-ui"] : ["pnpm install", "forgeax doctor"]
767
+ }
768
+ };
769
+ }
770
+ async function engineUseLocalCommand(options) {
771
+ const root = resolve(options.root ?? process.cwd());
772
+ if (options.path === void 0 || options.path.trim().length === 0) {
773
+ return bindingError(
774
+ "engine-binding-path-missing",
775
+ "forgeax engine use-local <engine-directory>",
776
+ "Pass the local Engine repository or SDK source directory."
777
+ );
778
+ }
779
+ let localPath;
780
+ try {
781
+ localPath = await realpath(resolve(root, options.path));
782
+ } catch (cause) {
783
+ return bindingError(
784
+ "engine-workspace-missing",
785
+ "the local Engine directory to exist",
786
+ "Pass an existing Engine repository or SDK source directory.",
787
+ {
788
+ path: resolve(root, options.path),
789
+ reason: cause instanceof Error ? cause.message : String(cause)
790
+ }
791
+ );
792
+ }
793
+ const workspace = await inspectEngineWorkspace(localPath);
794
+ if (!workspace.ok) return workspace;
795
+ const binding = {
796
+ schemaVersion: ENGINE_BINDING_SCHEMA_VERSION,
797
+ path: localPath
798
+ };
799
+ if (options.dryRun !== true) await writeEngineBinding(root, binding);
800
+ const status = await engineStatusCommand({ root });
801
+ if (!status.ok) return status;
802
+ return {
803
+ ok: true,
804
+ value: {
805
+ ...status.value,
806
+ next: workspace.value.missingBuilds.length === 0 ? ["forgeax build", "forgeax capture --backend auto --require-ui"] : ["pnpm build:engine", "forgeax engine doctor"]
807
+ }
808
+ };
809
+ }
810
+ async function engineUnlinkCommand(options = {}) {
811
+ const root = resolve(options.root ?? process.cwd());
812
+ if (options.dryRun !== true) await rm(bindingFile(root), { force: true });
813
+ return engineStatusCommand({ root });
814
+ }
815
+ async function engineDoctorCommand(options = {}) {
816
+ const status = await engineStatusCommand(options);
817
+ if (!status.ok) return status;
818
+ if (status.value.projectDependencies === "workspace" || status.value.projectDependencies === "mixed") {
819
+ return {
820
+ ok: false,
821
+ error: {
822
+ code: "engine-project-workspace-dependency",
823
+ expected: "the game project to use registry or SDK-resolved dependencies for npm consumers",
824
+ hint: "This project is a pnpm workspace; use pnpm here, or create a clean SDK game project for npm install.",
825
+ detail: { root: status.value.root, projectDependencies: status.value.projectDependencies }
826
+ }
827
+ };
828
+ }
829
+ if (status.value.diagnostic !== void 0) return { ok: false, error: status.value.diagnostic };
830
+ if (!status.value.healthy) {
831
+ return {
832
+ ok: false,
833
+ error: {
834
+ code: status.value.mode === "local" ? "engine-local-build-missing" : "engine-sdk-unresolved",
835
+ expected: "the selected Engine binding to resolve to built packages",
836
+ 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.",
837
+ detail: { status: status.value }
838
+ }
839
+ };
840
+ }
841
+ return status;
842
+ }
843
+ var ENGINE_BINDING_SCHEMA_VERSION, bindingFile;
844
+ var init_engine_binding = __esm({
845
+ "src/engine-binding.ts"() {
846
+ init_project();
847
+ ENGINE_BINDING_SCHEMA_VERSION = "1.0.0";
848
+ bindingFile = (root) => resolve(root, ".forgeax", "engine-binding.json");
849
+ }
850
+ });
851
+
852
+ // src/host.ts
853
+ var host_exports = {};
854
+ __export(host_exports, {
855
+ createEngineWorkspaceResolverForProject: () => createEngineWorkspaceResolverForProject,
856
+ createViteConfig: () => createViteConfig,
857
+ devKitDdcRoots: () => devKitDdcRoots,
858
+ ignoreDevKitCatalogPath: () => ignoreDevKitCatalogPath
859
+ });
860
+ function ignoreDevKitCatalogPath(path) {
861
+ const normalized = path.replace(/\\/g, "/");
862
+ return normalized.split("/").includes("shaders");
863
+ }
864
+ function devKitDdcRoots(projectRoot2) {
865
+ return {
866
+ buildCacheRoot: resolve(projectRoot2, ".forgeax", "ddc", "build-cache"),
867
+ projectDdcRoot: resolve(projectRoot2, ".forgeax", "ddc", "v2")
868
+ };
869
+ }
870
+ function isResourcePreviewIgnoredPath(path) {
871
+ return ignoreDevKitCatalogPath(path) || path.endsWith(".wgsl.meta.json") || path.endsWith("target-profile.json.meta.json");
872
+ }
873
+ function isProjectSourceIgnoredPath(path) {
874
+ return ignoreDevKitCatalogPath(path) || path.endsWith(".wgsl.meta.json");
875
+ }
876
+ async function prepareBuiltinPack(projectRoots, generated, ignorePath) {
877
+ const inventory = await scanInventory(projectRoots, { ignorePath });
878
+ if (!inventory.ok) return void 0;
879
+ const declared = /* @__PURE__ */ new Set();
880
+ for (const declaration of inventory.value.declarations.values()) {
881
+ if (declaration.format === "pack.json") {
882
+ for (const asset of declaration.value.assets) declared.add(asset.guid.toLowerCase());
883
+ continue;
884
+ }
885
+ for (const asset of declaration.value.subAssets) declared.add(asset.guid.toLowerCase());
886
+ }
887
+ const missing = BUILTIN_MESH_ASSETS.filter((asset) => !declared.has(asset.guid.toLowerCase()));
888
+ if (missing.length === 0) return void 0;
889
+ const packPath = resolve(generated, "engine-builtins.pack.json");
890
+ await writeFile(
891
+ packPath,
892
+ `${JSON.stringify(
893
+ {
894
+ schemaVersion: "2.0.0",
895
+ kind: "internal-text-package",
896
+ assets: missing.map((asset) => ({
897
+ guid: asset.guid,
898
+ kind: "mesh",
899
+ payload: { geometry: asset.geometry },
900
+ refs: [],
901
+ artifacts: {}
902
+ }))
903
+ },
904
+ null,
905
+ 2
906
+ )}
907
+ `,
908
+ "utf8"
909
+ );
910
+ return packPath;
911
+ }
912
+ function findEngineWorkspaceRoot() {
913
+ let cursor = dirname(fileURLToPath(import.meta.url));
914
+ for (; ; ) {
915
+ if (existsSync(resolve(cursor, "pnpm-workspace.yaml"))) return cursor;
916
+ const parent = dirname(cursor);
917
+ if (parent === cursor) return void 0;
918
+ cursor = parent;
919
+ }
920
+ }
921
+ async function engineWorkspacePackages(workspaceRoot = findEngineWorkspaceRoot()) {
922
+ if (workspaceRoot === void 0) return /* @__PURE__ */ new Map();
923
+ const packageRoot = resolve(workspaceRoot, "packages");
924
+ if (!existsSync(packageRoot)) return /* @__PURE__ */ new Map();
925
+ const packages = /* @__PURE__ */ new Map();
926
+ for (const entry of await readdir(packageRoot, { withFileTypes: true })) {
927
+ if (!entry.isDirectory()) continue;
928
+ const root = resolve(packageRoot, entry.name);
929
+ try {
930
+ const manifest = JSON.parse(await readFile(resolve(root, "package.json"), "utf8"));
931
+ if (manifest === null || typeof manifest !== "object") continue;
932
+ const name = manifest.name;
933
+ if (typeof name === "string" && name.startsWith("@forgeax/engine")) {
934
+ packages.set(name, { root, manifest });
935
+ }
936
+ } catch {
937
+ }
938
+ }
939
+ return packages;
940
+ }
941
+ function conditionalExport2(value) {
942
+ if (typeof value === "string") return value;
943
+ if (value === null || Array.isArray(value)) return void 0;
944
+ for (const condition of ["browser", "import", "node", "default"]) {
945
+ const candidate = value[condition];
946
+ if (candidate === void 0) continue;
947
+ const selected = conditionalExport2(candidate);
948
+ if (selected !== void 0) return selected;
949
+ }
950
+ return void 0;
951
+ }
952
+ function packageExportTarget(manifest, subpath) {
953
+ const exportsValue = manifest.exports;
954
+ if (exportsValue === void 0) {
955
+ if (subpath.length > 0) return void 0;
956
+ const main = manifest.module ?? manifest.main;
957
+ return typeof main === "string" ? main : void 0;
958
+ }
959
+ if (typeof exportsValue === "string" || exportsValue === null) {
960
+ return subpath.length === 0 ? conditionalExport2(exportsValue) : void 0;
961
+ }
962
+ const keys = Object.keys(exportsValue);
963
+ const subpathMap = keys.some((key) => key === "." || key.startsWith("./"));
964
+ if (!subpathMap) return subpath.length === 0 ? conditionalExport2(exportsValue) : void 0;
965
+ const requested = subpath.length === 0 ? "." : `./${subpath}`;
966
+ const exact = exportsValue[requested];
967
+ if (exact !== void 0) return conditionalExport2(exact);
968
+ for (const key of keys) {
969
+ const marker = key.indexOf("*");
970
+ if (marker < 0) continue;
971
+ const prefix = key.slice(0, marker);
972
+ const suffix = key.slice(marker + 1);
432
973
  if (!requested.startsWith(prefix) || !requested.endsWith(suffix)) continue;
433
974
  const replacement = requested.slice(prefix.length, requested.length - suffix.length);
434
975
  const exportTarget = exportsValue[key];
435
976
  if (exportTarget === void 0) continue;
436
- const selected = conditionalExport(exportTarget);
977
+ const selected = conditionalExport2(exportTarget);
437
978
  return selected?.replaceAll("*", replacement);
438
979
  }
439
980
  return void 0;
440
981
  }
441
982
  function engineWorkspaceImport(source, packages) {
442
- if (!source.startsWith("@forgeax/engine-")) return void 0;
443
- const separator = source.indexOf("/", "@forgeax/engine-".length);
983
+ if (!source.startsWith("@forgeax/engine")) return void 0;
984
+ const separator = source.indexOf("/", "@forgeax/engine".length);
444
985
  const packageName = separator < 0 ? source : source.slice(0, separator);
445
986
  const subpath = separator < 0 ? "" : source.slice(separator + 1);
446
987
  const packageInfo = packages.get(packageName);
@@ -454,14 +995,25 @@ function engineWorkspaceImport(source, packages) {
454
995
  }
455
996
  return absolute;
456
997
  }
457
- async function createEngineWorkspaceResolver() {
458
- const packages = await engineWorkspacePackages();
998
+ async function createEngineWorkspaceResolver(projectRoot2) {
999
+ const binding = await readEngineBinding(projectRoot2);
1000
+ if (!binding.ok) {
1001
+ throw new Error(`${binding.error.code}: ${binding.error.hint}`);
1002
+ }
1003
+ const localRoot = binding.value?.path;
1004
+ if (localRoot !== void 0) {
1005
+ const inspected = await inspectEngineWorkspace(localRoot);
1006
+ if (!inspected.ok) throw new Error(`${inspected.error.code}: ${inspected.error.hint}`);
1007
+ }
1008
+ const packages = await engineWorkspacePackages(localRoot);
459
1009
  if (packages.size === 0) return void 0;
460
1010
  return {
461
1011
  name: "forgeax:devkit-engine-workspace-resolver",
1012
+ enforce: "pre",
462
1013
  async resolveId(source, importer) {
463
1014
  const bareSource = source.split("?", 1)[0] ?? source;
464
- if (!bareSource.startsWith("@forgeax/engine-")) return null;
1015
+ if (!bareSource.startsWith("@forgeax/engine")) return null;
1016
+ if (localRoot !== void 0) return engineWorkspaceImport(bareSource, packages);
465
1017
  try {
466
1018
  const resolved = await this.resolve(source, importer, { skipSelf: true });
467
1019
  if (resolved !== null) return resolved;
@@ -471,6 +1023,9 @@ async function createEngineWorkspaceResolver() {
471
1023
  }
472
1024
  };
473
1025
  }
1026
+ async function createEngineWorkspaceResolverForProject(projectRoot2) {
1027
+ return createEngineWorkspaceResolver(projectRoot2);
1028
+ }
474
1029
  async function consumerEngineAliases(projectRoot2) {
475
1030
  const root = resolve(projectRoot2, "node_modules", ".pnpm", "node_modules", "@forgeax");
476
1031
  try {
@@ -713,7 +1268,11 @@ function prepareAssetRegistry(assets) {
713
1268
  const existing = assetPreparation.get(assets);
714
1269
  if (existing !== undefined) return existing;
715
1270
  const pending = (async () => {
716
- assets.configureRuntimeBinding(runtimeScopeBinding);
1271
+ if (import.meta.env.DEV) {
1272
+ assets.configureRuntimeBinding(runtimeScopeBinding);
1273
+ } else {
1274
+ assets.configurePackIndex(new URL('pack-index.json', document.baseURI).href);
1275
+ }
717
1276
  assets.setCatalogSource(assetCatalog);
718
1277
  if (vfxRuntimeHost !== undefined) {
719
1278
  assets.installDecoder(vfxGpuEffectContribution.kind, vfxGpuEffectContribution.decoder);
@@ -783,6 +1342,44 @@ async function previewOwnerFacts(assets, resource, payload) {
783
1342
  return undefined;
784
1343
  }
785
1344
 
1345
+ const gameProjectionDefinitions = new Map();
1346
+ function registerGameProjection(kind, definition) {
1347
+ if (gameProjectionDefinitions.has(definition.id)) {
1348
+ throw new Error('forgeax: duplicate game projection id ' + definition.id);
1349
+ }
1350
+ const entry = { kind, definition };
1351
+ gameProjectionDefinitions.set(definition.id, entry);
1352
+ return () => {
1353
+ if (gameProjectionDefinitions.get(definition.id) === entry) {
1354
+ gameProjectionDefinitions.delete(definition.id);
1355
+ }
1356
+ };
1357
+ }
1358
+ const gameProjection = {
1359
+ registerAction: (definition) => registerGameProjection('action', definition),
1360
+ registerRead: (definition) => registerGameProjection('read', definition),
1361
+ };
1362
+ function exposeGameInspection(app) {
1363
+ globalThis.__forgeaxGameInspection = {
1364
+ list() {
1365
+ return {
1366
+ reads: Array.from(gameProjectionDefinitions.entries())
1367
+ .filter(([, entry]) => entry.kind === 'read')
1368
+ .map(([id]) => id),
1369
+ };
1370
+ },
1371
+ async read(id) {
1372
+ const entry = gameProjectionDefinitions.get(id);
1373
+ if (entry?.kind !== 'read') throw new Error('forgeax: game read projection not found ' + id);
1374
+ return entry.definition.read();
1375
+ },
1376
+ renderer() {
1377
+ const inspection = app.renderer.inspect();
1378
+ return { state: inspection.state, frameId: inspection.frame.frameId };
1379
+ },
1380
+ };
1381
+ }
1382
+
786
1383
  async function prepareProject(app) {
787
1384
  await prepareAssetRegistry(app.assets);
788
1385
  previewWorld = app.world;
@@ -967,6 +1564,7 @@ await app.pluginContext.plugin(gameHostPlugin({
967
1564
  ...(defaultSceneRoot === undefined ? {} : { defaultSceneRoot }),
968
1565
  uiRoot: uiRoot instanceof HTMLElement ? uiRoot : document.body,
969
1566
  setPointerLockAllowed: (allowed) => app.input?.setPointerLockAllowed?.(allowed),
1567
+ ...(import.meta.env.DEV ? { gameProjection } : {}),
970
1568
  }));
971
1569
  const { loader: pluginLoader } = await installCatalogLoader(app.pluginContext, pluginCatalog, 'engine');
972
1570
  await pluginLoader.root.update(projectPluginEntries(pluginEntries, 'engine'));
@@ -1003,7 +1601,10 @@ if (query.has('forgeax-tool-replay')) {
1003
1601
  if (current === undefined) return current;
1004
1602
  if (resource?.kind !== 'vfx') {
1005
1603
  if (resource?.kind === 'material' || resource?.kind === 'mesh' || resource?.kind === 'texture') {
1006
- if (app.renderer.drawCalls <= 0) return current;
1604
+ const subjectDrawn = resource.kind === 'texture'
1605
+ ? app.renderer.drawCalls > 0
1606
+ : app.renderer.drawCalls > 2;
1607
+ if (!subjectDrawn) return current;
1007
1608
  if (resource.kind === 'mesh') {
1008
1609
  const asset = current.asset;
1009
1610
  const aabb = asset?.aabb;
@@ -1113,6 +1714,7 @@ if (query.has('forgeax-tool-replay')) {
1113
1714
  );
1114
1715
  if (!result.ok) throw result.error;
1115
1716
  const app = result.value;
1717
+ if (import.meta.env.DEV) exposeGameInspection(app);
1116
1718
  await prepareProject(app);
1117
1719
  ${bootstrapPlugin}
1118
1720
  app.start().unwrap();
@@ -1166,32 +1768,62 @@ function htmlSource(title) {
1166
1768
  <div id="app-shell"><canvas id="app"></canvas><div id="game-ui"></div></div><div id="forgeax-fatal" role="alert"></div>
1167
1769
  <script>
1168
1770
  (() => {
1771
+ const appendStructuredFailure = (value, prefix, depth, seen, lines) => {
1772
+ if (value === null || typeof value !== 'object' || depth > 3) return;
1773
+ if (seen.has(value)) {
1774
+ lines.push((prefix || 'cause') + ': [circular]');
1775
+ return;
1776
+ }
1777
+ seen.add(value);
1778
+ const record = value;
1779
+ const name = typeof record.name === 'string' && record.name.length > 0
1780
+ ? record.name
1781
+ : undefined;
1782
+ const code = typeof record.code === 'string' && record.code.length > 0
1783
+ ? record.code
1784
+ : undefined;
1785
+ const message = typeof record.message === 'string' && record.message.length > 0
1786
+ ? record.message
1787
+ : undefined;
1788
+ if (name !== undefined || code !== undefined || message !== undefined) {
1789
+ const identity = [name || 'Error', code].filter(Boolean).join(' ');
1790
+ lines.push((prefix ? prefix + ': ' : '') + identity + (message ? ': ' + message : ''));
1791
+ }
1792
+ for (const key of ['expected', 'hint', 'reason']) {
1793
+ if (typeof record[key] === 'string' && record[key].length > 0) {
1794
+ lines.push((prefix ? prefix + '.' : '') + key + ': ' + record[key]);
1795
+ }
1796
+ }
1797
+ for (const key of ['cause', 'detail', 'webgpuError', 'wgpuError', 'error']) {
1798
+ const nested = record[key];
1799
+ const nestedPrefix = (prefix ? prefix + '.' : '') + key;
1800
+ if (nested !== null && typeof nested === 'object') {
1801
+ appendStructuredFailure(nested, nestedPrefix, depth + 1, seen, lines);
1802
+ } else if (typeof nested === 'string' && nested.length > 0) {
1803
+ lines.push(nestedPrefix + ': ' + nested);
1804
+ }
1805
+ }
1806
+ };
1169
1807
  const formatStartupFailure = (reason) => {
1170
- if (reason instanceof Error) return reason.message;
1171
1808
  if (reason !== null && typeof reason === 'object') {
1172
- const record = reason;
1173
1809
  const lines = [];
1174
- for (const key of ['code', 'expected', 'hint']) {
1175
- if (typeof record[key] === 'string' && record[key].length > 0) {
1176
- lines.push(key + ': ' + record[key]);
1177
- }
1178
- }
1179
- const detail = record.detail;
1180
- if (detail !== null && typeof detail === 'object') {
1181
- for (const key of ['reason', 'guid']) {
1182
- if (typeof detail[key] === 'string' && detail[key].length > 0) {
1183
- lines.push('detail.' + key + ': ' + detail[key]);
1184
- }
1185
- }
1186
- }
1810
+ appendStructuredFailure(reason, '', 0, new Set(), lines);
1187
1811
  if (lines.length > 0) return lines.join('\\n');
1812
+ try {
1813
+ return JSON.stringify(reason) || 'Unknown structured startup failure';
1814
+ } catch {
1815
+ return 'Unserializable structured startup failure';
1816
+ }
1188
1817
  }
1189
1818
  return String(reason ?? 'Unknown startup failure');
1190
1819
  };
1191
1820
  const show = (reason) => {
1192
1821
  const notice = document.querySelector('#forgeax-fatal');
1193
1822
  if (!(notice instanceof HTMLElement)) return;
1194
- const message = formatStartupFailure(reason);
1823
+ let message = formatStartupFailure(reason);
1824
+ if (/webgpu|adapter-unavailable|no usable (rendering )?backend/i.test(message)) {
1825
+ 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.';
1826
+ }
1195
1827
  notice.textContent = 'ForgeaX game failed to start.\\n' + message;
1196
1828
  notice.style.display = 'grid';
1197
1829
  };
@@ -1223,10 +1855,11 @@ async function createViteConfig(facts, command2, base = "/", options = {}) {
1223
1855
  ];
1224
1856
  const importers = [...projectImporters(facts)];
1225
1857
  const runtimeBinding = createStandaloneRuntimeAssetBinding(facts.id);
1226
- const engineWorkspaceResolver = await createEngineWorkspaceResolver();
1858
+ const engineWorkspaceResolver = await createEngineWorkspaceResolver(facts.root);
1227
1859
  const consumerAliases = await consumerEngineAliases(facts.root);
1228
1860
  const plugins = [
1229
1861
  ...engineWorkspaceResolver === void 0 ? [] : [engineWorkspaceResolver],
1862
+ ...command2 === "serve" && process.env.FORGEAX_ENGINE_RHI_DEBUG === "1" ? [vitePluginRhiDebug({ rootDir: facts.root })] : [],
1230
1863
  forgeaxShader(),
1231
1864
  pluginPack({
1232
1865
  roots,
@@ -1264,152 +1897,742 @@ async function createViteConfig(facts, command2, base = "/", options = {}) {
1264
1897
  }
1265
1898
  };
1266
1899
  }
1267
- var hostRequire, engineWorkspacePackagesPromise;
1900
+ var hostRequire;
1268
1901
  var init_host = __esm({
1269
1902
  "src/host.ts"() {
1903
+ init_engine_binding();
1270
1904
  hostRequire = createRequire(import.meta.url);
1271
1905
  }
1272
1906
  });
1273
- function projectError(code, expected, hint, detail) {
1274
- return { ok: false, error: { code, expected, hint, detail } };
1907
+ function errorResult(code, expected, hint, detail = {}) {
1908
+ return { ok: false, error: new SingleHtmlError(code, expected, hint, detail) };
1275
1909
  }
1276
- async function readJson(path) {
1277
- return JSON.parse(await readFile(path, "utf8"));
1910
+ function toErrorResult(cause, code, expected, hint) {
1911
+ if (cause instanceof SingleHtmlError) return { ok: false, error: cause };
1912
+ return errorResult(code, expected, hint, {
1913
+ reason: cause instanceof Error ? cause.message : String(cause)
1914
+ });
1278
1915
  }
1279
- function firstUnsupportedStandaloneRealm(entries2, inheritedRealm = "engine") {
1280
- for (const entry of entries2) {
1281
- const realm = entry.realm ?? inheritedRealm ?? "engine";
1282
- if (realm !== "engine") return { id: entry.id, realm };
1283
- if (entry.group === true) {
1284
- const unsupported = firstUnsupportedStandaloneRealm(
1285
- entry.config,
1286
- realm
1287
- );
1288
- if (unsupported !== void 0) return unsupported;
1289
- }
1916
+ function normalizePath(value) {
1917
+ let decoded;
1918
+ try {
1919
+ decoded = decodeURIComponent(value.split(/[?#]/, 1)[0] ?? value);
1920
+ } catch {
1921
+ return void 0;
1290
1922
  }
1291
- return void 0;
1923
+ const normalized = decoded.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
1924
+ const segments = normalized.split("/");
1925
+ if (normalized.length === 0 || segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) {
1926
+ return void 0;
1927
+ }
1928
+ return normalized;
1292
1929
  }
1293
- function pluginModuleNames(entries2) {
1294
- return entries2.flatMap(
1295
- (entry) => entry.group === true ? pluginModuleNames(entry.config) : [entry.name]
1930
+ function safeScriptText(value) {
1931
+ return value.replace(/<\/script/gi, "<\\/script");
1932
+ }
1933
+ function hashBytes(bytes) {
1934
+ return createHash("sha256").update(bytes).digest("hex");
1935
+ }
1936
+ function contentTypeForDataUri(value) {
1937
+ const semicolon = value.indexOf(";");
1938
+ return semicolon < 0 ? value : value.slice(0, semicolon);
1939
+ }
1940
+ function dataUri(bytes, type) {
1941
+ return `data:${contentTypeForDataUri(type)};base64,${Buffer.from(bytes).toString("base64")}`;
1942
+ }
1943
+ function attributeValueRange(source, location) {
1944
+ const raw = source.slice(location.startOffset, location.endOffset);
1945
+ const equals = raw.indexOf("=");
1946
+ if (equals < 0) return void 0;
1947
+ let cursor = equals + 1;
1948
+ while (/\s/.test(raw[cursor] ?? "")) cursor += 1;
1949
+ const quote = raw[cursor] === '"' || raw[cursor] === "'" ? raw[cursor] : void 0;
1950
+ if (quote !== void 0) {
1951
+ const start = cursor + 1;
1952
+ const end = raw.lastIndexOf(quote);
1953
+ return end <= start ? void 0 : { startOffset: location.startOffset + start, endOffset: location.startOffset + end };
1954
+ }
1955
+ return {
1956
+ startOffset: location.startOffset + cursor,
1957
+ endOffset: location.endOffset
1958
+ };
1959
+ }
1960
+ function scanHtmlElements(source) {
1961
+ const document2 = parse(source, { sourceCodeLocationInfo: true });
1962
+ const elements = [];
1963
+ const visit = (node) => {
1964
+ if (node.nodeName === "#document" || node.nodeName === "#document-fragment") {
1965
+ for (const child of node.childNodes) visit(child);
1966
+ return;
1967
+ }
1968
+ if (node.nodeName === "#text" || node.nodeName === "#comment" || node.nodeName === "#documentType") {
1969
+ return;
1970
+ }
1971
+ const element = node;
1972
+ const location = element.sourceCodeLocation;
1973
+ if (location?.startTag === void 0) {
1974
+ for (const child of element.childNodes) visit(child);
1975
+ return;
1976
+ }
1977
+ const name = element.tagName.toLowerCase();
1978
+ const attributes = element.attrs.map(({ name: attributeName, value }) => ({
1979
+ name: attributeName.toLowerCase(),
1980
+ value
1981
+ }));
1982
+ const attributeLocations = {};
1983
+ for (const attribute2 of attributes) {
1984
+ const attributeLocation = location.attrs?.[attribute2.name];
1985
+ if (attributeLocation !== void 0) attributeLocations[attribute2.name] = attributeLocation;
1986
+ }
1987
+ elements.push({
1988
+ name,
1989
+ start: location.startTag.startOffset,
1990
+ openEnd: location.startTag.endOffset,
1991
+ end: location.endTag?.endOffset ?? location.startTag.endOffset,
1992
+ ...name === "script" || name === "style" ? {
1993
+ contentStart: location.startTag.endOffset,
1994
+ contentEnd: location.endTag?.startOffset ?? location.startTag.endOffset
1995
+ } : {},
1996
+ attributes,
1997
+ attributeLocations
1998
+ });
1999
+ for (const child of element.childNodes) visit(child);
2000
+ };
2001
+ visit(document2);
2002
+ return elements;
2003
+ }
2004
+ function attribute(element, name) {
2005
+ return element.attributes.find((candidate) => candidate.name === name);
2006
+ }
2007
+ function attributeValue(element, name) {
2008
+ return attribute(element, name)?.value;
2009
+ }
2010
+ function isModuleScript(element) {
2011
+ return element.name === "script" && attributeValue(element, "type")?.toLowerCase() === "module";
2012
+ }
2013
+ function isStylesheet(element) {
2014
+ return element.name === "link" && attributeValue(element, "rel")?.toLowerCase().split(/\s+/).includes("stylesheet") === true;
2015
+ }
2016
+ function isModulePreload(element) {
2017
+ return element.name === "link" && attributeValue(element, "rel")?.toLowerCase().split(/\s+/).includes("modulepreload") === true;
2018
+ }
2019
+ function resourceForPath(path, resources) {
2020
+ const normalized = normalizePath(path);
2021
+ if (normalized === void 0) return void 0;
2022
+ const direct = resources.get(normalized);
2023
+ if (direct !== void 0) return direct;
2024
+ const suffix = [...resources.entries()].filter(
2025
+ ([candidate]) => candidate.endsWith(`/${normalized}`)
1296
2026
  );
2027
+ return suffix.length === 1 ? suffix[0]?.[1] : void 0;
1297
2028
  }
1298
- async function readProjectFacts(rootInput = process.cwd()) {
1299
- const root = resolve(rootInput);
1300
- let forgeValue;
1301
- let packageValue;
2029
+ function resourcePath(reference, basePath) {
1302
2030
  try {
1303
- [forgeValue, packageValue] = await Promise.all([
1304
- readJson(resolve(root, "forge.json")),
1305
- readJson(resolve(root, "package.json"))
1306
- ]);
1307
- } catch (cause) {
1308
- return projectError(
1309
- "project-manifest-unreadable",
1310
- "readable forge.json and package.json files",
1311
- "Run the command from a ForgeaX game root or pass its directory.",
1312
- { root, reason: cause instanceof Error ? cause.message : String(cause) }
1313
- );
2031
+ return normalizePath(new URL(reference, `https://forgeax.invalid/${basePath}`).pathname);
2032
+ } catch {
2033
+ return void 0;
1314
2034
  }
1315
- if (forgeValue === null || typeof forgeValue !== "object") {
1316
- return projectError(
1317
- "project-manifest-invalid",
1318
- "forge.json to contain an object",
1319
- "Repair forge.json before running DevKit.",
1320
- { root }
1321
- );
2035
+ }
2036
+ function applyReplacements(source, replacements) {
2037
+ const unique = /* @__PURE__ */ new Map();
2038
+ for (const replacement of replacements) {
2039
+ unique.set(`${replacement.start}:${replacement.end}`, replacement);
1322
2040
  }
1323
- if (packageValue === null || typeof packageValue !== "object") {
1324
- return projectError(
1325
- "package-manifest-invalid",
1326
- "package.json to contain an object",
1327
- "Repair package.json before running DevKit.",
1328
- { root }
1329
- );
2041
+ const ordered = [...unique.values()].sort((left, right) => left.start - right.start);
2042
+ for (let index = 1; index < ordered.length; index += 1) {
2043
+ const previous = ordered[index - 1];
2044
+ const current = ordered[index];
2045
+ if (previous !== void 0 && current !== void 0 && previous.end > current.start) {
2046
+ throw new Error(
2047
+ `single-html AST replacements overlap (${previous.start}:${previous.end} and ${current.start}:${current.end})`
2048
+ );
2049
+ }
1330
2050
  }
1331
- const parsedForge = GameProjectSchema.safeParse(forgeValue);
1332
- if (!parsedForge.success) {
1333
- return projectError(
1334
- "project-manifest-invalid",
1335
- "forge.json to satisfy @forgeax/engine-project GameProjectSchema",
1336
- "Repair the fields reported by the authoritative project schema.",
1337
- { root, issues: parsedForge.error.issues }
2051
+ return ordered.reverse().reduce(
2052
+ (value, replacement) => value.slice(0, replacement.start) + replacement.value + value.slice(replacement.end),
2053
+ source
2054
+ );
2055
+ }
2056
+ function inlineCss(css, cssPath, resources) {
2057
+ const replacements = [];
2058
+ const pattern = /url\(\s*(["']?)(.*?)\1\s*\)/gi;
2059
+ for (; ; ) {
2060
+ const match = pattern.exec(css);
2061
+ if (match === null) break;
2062
+ const reference = match[2]?.trim();
2063
+ if (reference === void 0 || reference.length === 0 || reference.startsWith("data:") || reference.startsWith("#")) {
2064
+ continue;
2065
+ }
2066
+ if (/^(?:https?:|blob:)/i.test(reference)) {
2067
+ return errorResult(
2068
+ "single-html-css-external-resource",
2069
+ "CSS URL references to be embedded or data/blob URLs",
2070
+ "Move the CSS resource into the verified dist closure before packaging.",
2071
+ { cssPath, reference }
2072
+ );
2073
+ }
2074
+ const path = resourcePath(reference, cssPath);
2075
+ const resource = path === void 0 ? void 0 : resourceForPath(path, resources);
2076
+ if (resource === void 0) {
2077
+ return errorResult(
2078
+ "single-html-css-asset-missing",
2079
+ "every local CSS URL to resolve to an embedded dist artifact",
2080
+ "Add the referenced asset to the project asset closure and rebuild.",
2081
+ { cssPath, reference, path: path ?? null }
2082
+ );
2083
+ }
2084
+ replacements.push({
2085
+ start: match.index,
2086
+ end: match.index + match[0].length,
2087
+ value: `url(${dataUri(resource.bytes, resource.mediaType)})`
2088
+ });
2089
+ }
2090
+ return { ok: true, value: applyReplacements(css, replacements) };
2091
+ }
2092
+ function collectEntrySource(html) {
2093
+ const entries2 = scanHtmlElements(html).filter((element2) => isModuleScript(element2));
2094
+ const external = entries2.filter((element2) => attributeValue(element2, "src") !== void 0);
2095
+ const inline = entries2.filter((element2) => attributeValue(element2, "src") === void 0);
2096
+ if (external.length === 0 && inline.length === 0) {
2097
+ return errorResult(
2098
+ "single-html-entry-missing",
2099
+ "production index.html to contain one module entry",
2100
+ "Rebuild the game with a module entry in its generated host."
1338
2101
  );
1339
2102
  }
1340
- const forge = parsedForge.data;
1341
- if (forge.id.length === 0 || forge.name.length === 0 || forge.entry === void 0 || forge.entry.length === 0) {
1342
- return projectError(
1343
- "project-manifest-invalid",
1344
- "forge.json to declare id, name, and entry",
1345
- "Restore the project entry; plugin Entries remain optional additions.",
1346
- { root }
2103
+ if (external.length > 1 || inline.length > 1) {
2104
+ return errorResult(
2105
+ "single-html-entry-ambiguous",
2106
+ "production index.html to contain exactly one module entry",
2107
+ "Converge the generated host to one ForgeaX module entry before packaging.",
2108
+ { external: external.length, inline: inline.length }
1347
2109
  );
1348
2110
  }
1349
- const plugins = forge.plugins ?? [];
1350
- const unsupportedRealm = firstUnsupportedStandaloneRealm(plugins);
1351
- if (unsupportedRealm !== void 0) {
1352
- return projectError(
1353
- "project-plugin-realm-unsupported",
1354
- "the standalone Devkit host to contain only engine-realm plugin Entries",
1355
- "Move Host or build plugins to a host that owns that physical realm.",
1356
- { root, ...unsupportedRealm }
2111
+ const element = external[0] ?? inline[0];
2112
+ if (element === void 0) throw new Error("single-html entry selection was unexpectedly empty");
2113
+ const src = attributeValue(element, "src");
2114
+ if (src === void 0 && element.contentStart !== void 0 && element.contentEnd !== void 0) {
2115
+ return { ok: true, value: html.slice(element.contentStart, element.contentEnd) };
2116
+ }
2117
+ if (src === void 0) {
2118
+ return errorResult(
2119
+ "single-html-entry-missing",
2120
+ "a module entry source",
2121
+ "Add a module entry to index.html."
1357
2122
  );
1358
2123
  }
1359
- const entryPath = isAbsolute(forge.entry) ? forge.entry : resolve(root, forge.entry);
1360
- try {
1361
- await readFile(entryPath);
1362
- } catch {
1363
- return projectError(
1364
- "project-entry-missing",
1365
- "forge.json#entry to resolve to a readable module",
1366
- "Restore the game entry or update forge.json#entry.",
1367
- { root, entry: forge.entry }
2124
+ if (/^(?:https?:|file:|data:|blob:)/i.test(src)) {
2125
+ return errorResult(
2126
+ "single-html-entry-external",
2127
+ "the module entry to be a project-local dist artifact",
2128
+ "Rebuild the game so the generated host points at a local production module.",
2129
+ { src }
1368
2130
  );
1369
2131
  }
1370
- const packageJson = packageValue;
1371
- const forgeax = packageJson.forgeax;
1372
- const configuredRoots = forgeax !== null && typeof forgeax === "object" ? forgeax.assets?.roots : void 0;
1373
- const assetRoots = Array.isArray(configuredRoots) && configuredRoots.every((value) => typeof value === "string") ? configuredRoots : ["assets"];
1374
- const configuredImporters = forgeax !== null && typeof forgeax === "object" ? forgeax.assets?.importers : void 0;
1375
- const assetImporters = Array.isArray(configuredImporters) && configuredImporters.every((value) => typeof value === "string") ? configuredImporters : [];
1376
- const configuredPublicDir = forgeax !== null && typeof forgeax === "object" ? forgeax.assets?.publicDir : void 0;
1377
- const assetPublicDir = typeof configuredPublicDir === "string" ? configuredPublicDir : void 0;
1378
- const physics = forge.physics === "2d" || forge.physics === "3d" ? forge.physics : void 0;
1379
- const defaultScene = typeof forge.defaultScene === "string" && forge.defaultScene.length > 0 ? forge.defaultScene : void 0;
1380
- const normalizedEntry = forge.entry.startsWith("./") ? forge.entry : `./${forge.entry}`;
1381
- const bootstrapEntry = pluginModuleNames(plugins).map((name) => name.startsWith("./") ? name : `./${name}`).includes(normalizedEntry) ? void 0 : forge.entry;
1382
- return {
1383
- ok: true,
1384
- value: {
1385
- root,
1386
- id: forge.id,
1387
- name: forge.name,
1388
- entry: forge.entry,
1389
- ...bootstrapEntry === void 0 ? {} : { bootstrapEntry },
1390
- plugins,
1391
- ...physics === void 0 ? {} : { physics },
1392
- ...defaultScene === void 0 ? {} : { defaultScene },
1393
- assetRoots,
1394
- ...assetImporters.length === 0 ? {} : { assetImporters },
1395
- ...assetPublicDir === void 0 ? {} : { assetPublicDir },
1396
- packageJson
2132
+ return { ok: true, value: src };
2133
+ }
2134
+ async function filesUnder2(root, directory = root) {
2135
+ const result = [];
2136
+ for (const name of (await readdir(directory)).sort()) {
2137
+ const path = resolve(directory, name);
2138
+ const info = await stat(path);
2139
+ if (info.isDirectory()) result.push(...await filesUnder2(root, path));
2140
+ else if (info.isFile()) result.push(path);
2141
+ }
2142
+ return result;
2143
+ }
2144
+ function isPreloadHelperSource(value) {
2145
+ if (typeof value !== "string") return false;
2146
+ const path = value.split(/[?#]/, 1)[0] ?? value;
2147
+ const name = path.slice(path.lastIndexOf("/") + 1);
2148
+ return name.startsWith("preload-helper-") && name.endsWith(".js");
2149
+ }
2150
+ function isStaticImportSource(source) {
2151
+ return source.type === "Literal" || source.type === "StringLiteral" || source.type === "TemplateLiteral" && source.expressions?.length === 0;
2152
+ }
2153
+ function programReplacements(program) {
2154
+ const replacements = [];
2155
+ const preloadBindings = /* @__PURE__ */ new Set();
2156
+ new Visitor({
2157
+ ImportDeclaration(node) {
2158
+ if (!isPreloadHelperSource(node.source.value)) return;
2159
+ for (const specifier of node.specifiers) {
2160
+ if (specifier.type === "ImportSpecifier") preloadBindings.add(specifier.local.name);
2161
+ }
2162
+ },
2163
+ CallExpression(node) {
2164
+ if (node.callee.type !== "Identifier" || !preloadBindings.has(node.callee.name) || node.arguments.length < 2) {
2165
+ return;
2166
+ }
2167
+ const dependencies = node.arguments[1];
2168
+ if (dependencies !== void 0) {
2169
+ replacements.push({ start: dependencies.start, end: dependencies.end, value: "void 0" });
2170
+ }
2171
+ },
2172
+ ImportExpression(node) {
2173
+ if (isStaticImportSource(node.source)) return;
2174
+ replacements.push({
2175
+ start: node.start,
2176
+ end: node.start + "import".length,
2177
+ value: "globalThis.__forgeaxImport"
2178
+ });
1397
2179
  }
2180
+ }).visit(program);
2181
+ return replacements;
2182
+ }
2183
+ function outputReplacements(program) {
2184
+ const replacements = [];
2185
+ new Visitor({
2186
+ ImportExpression(node) {
2187
+ replacements.push({
2188
+ start: node.start,
2189
+ end: node.start + "import".length,
2190
+ value: "globalThis.__forgeaxImport"
2191
+ });
2192
+ }
2193
+ }).visit(program);
2194
+ return replacements;
2195
+ }
2196
+ function rewriteOutputJavaScript(code) {
2197
+ const program = parseAst(code);
2198
+ return applyReplacements(code, outputReplacements(program));
2199
+ }
2200
+ function residualImportStart(code) {
2201
+ let residualStart;
2202
+ new Visitor({
2203
+ ImportExpression(node) {
2204
+ residualStart ??= node.start;
2205
+ }
2206
+ }).visit(parseAst(code));
2207
+ return residualStart;
2208
+ }
2209
+ function isJavaScriptOutputAsset(fileName) {
2210
+ return /\.(?:c?js|mjs)$/i.test(fileName);
2211
+ }
2212
+ function singleHtmlBundlePlugin() {
2213
+ const plugin = {
2214
+ name: "forgeax-single-html-bundle-runtime",
2215
+ enforce: "post",
2216
+ transform: {
2217
+ order: "post",
2218
+ handler(code, _id, meta) {
2219
+ let program = meta?.ast;
2220
+ if (program === void 0) {
2221
+ try {
2222
+ program = parseAst(code);
2223
+ } catch {
2224
+ return null;
2225
+ }
2226
+ }
2227
+ const replacements = programReplacements(program);
2228
+ if (replacements.length === 0) return null;
2229
+ if (meta?.magicString !== void 0) {
2230
+ for (const replacement of replacements) {
2231
+ meta.magicString.overwrite(replacement.start, replacement.end, replacement.value);
2232
+ }
2233
+ return meta.magicString.hasChanged() ? meta.magicString : null;
2234
+ }
2235
+ return applyReplacements(code, replacements);
2236
+ }
2237
+ },
2238
+ generateBundle(_outputOptions, bundle) {
2239
+ for (const output of Object.values(bundle)) {
2240
+ if (output.type === "chunk") {
2241
+ output.code = rewriteOutputJavaScript(output.code);
2242
+ } else if (isJavaScriptOutputAsset(output.fileName) && output.source !== void 0) {
2243
+ const source = typeof output.source === "string" ? output.source : Buffer.from(output.source).toString("utf8");
2244
+ output.source = rewriteOutputJavaScript(source);
2245
+ } else {
2246
+ continue;
2247
+ }
2248
+ const code = output.type === "chunk" ? output.code : String(output.source);
2249
+ const residualStart = residualImportStart(code);
2250
+ if (residualStart !== void 0) {
2251
+ this.error(
2252
+ `single-html bundle left a native dynamic import in ${output.fileName} at ${residualStart}`
2253
+ );
2254
+ }
2255
+ }
2256
+ },
2257
+ resolveFileUrl: ({ fileName }) => JSON.stringify(`${GENERATED_PREFIX}${fileName}`)
1398
2258
  };
2259
+ return plugin;
2260
+ }
2261
+ async function bundleSingleHtmlEntry(distRootInput, indexHtml, projectRoot2) {
2262
+ const distRoot = resolve(distRootInput);
2263
+ const selected = collectEntrySource(indexHtml);
2264
+ if (!selected.ok) return selected;
2265
+ const source = selected.value;
2266
+ const entryPath = normalizePath(source);
2267
+ const temporaryRoot = await mkdtemp(resolve(tmpdir(), "forgeax-single-html-bundle-"));
2268
+ try {
2269
+ let inputPath;
2270
+ if (entryPath !== void 0) {
2271
+ const candidate = resolve(distRoot, entryPath);
2272
+ try {
2273
+ const candidateInfo = await stat(candidate);
2274
+ if (candidateInfo.isFile()) inputPath = candidate;
2275
+ } catch {
2276
+ }
2277
+ }
2278
+ if (inputPath === void 0) {
2279
+ inputPath = resolve(temporaryRoot, "inline-entry.mjs");
2280
+ await writeFile(inputPath, source, "utf8");
2281
+ }
2282
+ const outputRoot = resolve(temporaryRoot, "dist");
2283
+ const resolver = projectRoot2 === void 0 ? void 0 : await createEngineWorkspaceResolverForProject(projectRoot2);
2284
+ await build({
2285
+ configFile: false,
2286
+ root: distRoot,
2287
+ base: "./",
2288
+ logLevel: "error",
2289
+ plugins: [singleHtmlBundlePlugin(), ...resolver === void 0 ? [] : [resolver]],
2290
+ experimental: {
2291
+ renderBuiltUrl: (filename) => `forgeax-resource:///${GENERATED_PREFIX}${filename}`
2292
+ },
2293
+ build: {
2294
+ outDir: outputRoot,
2295
+ emptyOutDir: true,
2296
+ target: "esnext",
2297
+ minify: false,
2298
+ sourcemap: false,
2299
+ assetsInlineLimit: 0,
2300
+ modulePreload: false,
2301
+ rolldownOptions: {
2302
+ input: inputPath,
2303
+ output: {
2304
+ codeSplitting: false,
2305
+ entryFileNames: "entry.mjs",
2306
+ chunkFileNames: "assets/[name]-[hash].mjs",
2307
+ assetFileNames: "assets/[name]-[hash][extname]"
2308
+ },
2309
+ experimental: { nativeMagicString: true }
2310
+ }
2311
+ }
2312
+ });
2313
+ const paths = (await filesUnder2(outputRoot)).map(
2314
+ (path) => relative(outputRoot, path).split(sep).join("/")
2315
+ );
2316
+ const entry = paths.find((path) => path === "entry.mjs");
2317
+ if (entry === void 0) {
2318
+ return errorResult(
2319
+ "single-html-bundle-entry-missing",
2320
+ "Vite to emit exactly one entry.mjs",
2321
+ "Inspect the production entry and the single-html bundler output.",
2322
+ { outputRoot, paths }
2323
+ );
2324
+ }
2325
+ const generatedPaths = paths.filter((path) => path !== entry);
2326
+ const artifacts = [];
2327
+ for (const path of generatedPaths) {
2328
+ const bytes = await readFile(resolve(outputRoot, path));
2329
+ artifacts.push({
2330
+ path: `${GENERATED_PREFIX}${path}`,
2331
+ bytes,
2332
+ mediaType: mediaType(path)
2333
+ });
2334
+ }
2335
+ const entryBytes = await readFile(resolve(outputRoot, entry));
2336
+ return {
2337
+ ok: true,
2338
+ value: {
2339
+ entrySource: entryBytes.toString("utf8"),
2340
+ artifacts
2341
+ }
2342
+ };
2343
+ } catch (cause) {
2344
+ return toErrorResult(
2345
+ cause,
2346
+ "single-html-bundle-failed",
2347
+ "the production module entry to converge into one executable bundle",
2348
+ "Repair the production entry or inspect the Vite/Rolldown diagnostic before packaging."
2349
+ );
2350
+ } finally {
2351
+ await rm(temporaryRoot, { recursive: true, force: true });
2352
+ }
1399
2353
  }
1400
- function commandError(cause, fallbackCode) {
1401
- if (cause !== null && typeof cause === "object" && "code" in cause && "expected" in cause && "hint" in cause && "detail" in cause) {
1402
- return cause;
2354
+ function workerPrelude(resources) {
2355
+ const payload = resources.map((resource) => ({
2356
+ path: resource.path,
2357
+ mime: resource.mediaType,
2358
+ data: Buffer.from(resource.bytes).toString("base64")
2359
+ }));
2360
+ const template = `globalThis.process??={env:{},versions:{node:'0.0.0'},platform:'browser',argv:[]};
2361
+ const __forgeaxWorkerPayload=${JSON.stringify(payload)};
2362
+ const __forgeaxSourcePath=__FORGEAX_SOURCE_PATH__;
2363
+ const __forgeaxNativeURL=globalThis.URL;
2364
+ const __forgeaxNativeFetch=globalThis.fetch?.bind(globalThis);
2365
+ const __forgeaxNativePostMessage=globalThis.postMessage?.bind(globalThis);
2366
+ const __forgeaxOwnedUrls=new Set();
2367
+ const __forgeaxBytes=(value)=>{const binary=atob(value);const bytes=new Uint8Array(binary.length);for(let index=0;index<binary.length;index+=1)bytes[index]=binary.charCodeAt(index);return bytes;};
2368
+ const __forgeaxDataURL=(source)=>{const bytes=new TextEncoder().encode(source);let binary='';for(const byte of bytes)binary+=String.fromCharCode(byte);return 'data:text/javascript;base64,'+btoa(binary);};
2369
+ const __forgeaxWorkerURL=(source)=>{if(/^(?:file:|data:|about:)/i.test(String(globalThis.location?.protocol??'')))return __forgeaxDataURL(source);const blob=__forgeaxNativeURL.createObjectURL(new Blob([source],{type:'text/javascript'}));__forgeaxOwnedUrls.add(blob);return blob;};
2370
+ const __forgeaxNormalize=(value)=>{try{return decodeURIComponent(String(value).split(/[?#]/,1)[0]).replaceAll('\\\\','/').replace(/^\\/+/, '');}catch{return undefined;}};
2371
+ const __forgeaxWithoutBundle=(value)=>String(value).replace(/^__forgeax-bundle\\//,'');
2372
+ const __forgeaxLookup=(value)=>{let url;try{url=value instanceof __forgeaxNativeURL?value:new __forgeaxNativeURL(String(value),'https://forgeax.invalid/'+__forgeaxSourcePath);}catch{return undefined;}const path=__forgeaxNormalize(url.pathname);if(path===undefined)return undefined;const normalized=__forgeaxWithoutBundle(path);const exact=__forgeaxWorkerPayload.find((entry)=>entry.path===path||__forgeaxWithoutBundle(entry.path)===normalized);if(exact!==undefined)return exact;const suffix=__forgeaxWorkerPayload.filter((entry)=>normalized.endsWith('/'+__forgeaxWithoutBundle(entry.path)));if(suffix.length===1)return suffix[0];const name=normalized.slice(normalized.lastIndexOf('/')+1);const matches=__forgeaxWorkerPayload.filter((entry)=>__forgeaxWithoutBundle(entry.path).slice(__forgeaxWithoutBundle(entry.path).lastIndexOf('/')+1)===name);return matches.length===1?matches[0]:undefined;};
2373
+ const __forgeaxResourceURL=(value)=>{const raw=String(value);try{const url=new __forgeaxNativeURL(raw,'https://forgeax.invalid/'+__forgeaxSourcePath);if(url.protocol!=='http:'&&url.protocol!=='https:')return url;const path=__forgeaxNormalize(url.pathname);return path===undefined?url:new __forgeaxNativeURL('forgeax-resource:///'+__forgeaxWithoutBundle(path));}catch{return new __forgeaxNativeURL(raw,'https://forgeax.invalid/'+__forgeaxSourcePath);}};
2374
+ globalThis.__forgeaxURL=__forgeaxResourceURL;
2375
+ class __ForgeaxURL extends __forgeaxNativeURL{constructor(input,base){if(base!==undefined&&/^blob:/i.test(String(base))&&!/^[a-z][a-z0-9+.-]*:/i.test(String(input))){super(__forgeaxResourceURL(input));return;}super(input,base);}}
2376
+ globalThis.URL=__ForgeaxURL;
2377
+ const __forgeaxNote=(kind,specifier)=>{const value={kind,specifier:String(specifier)};try{__forgeaxNativePostMessage?.({__forgeaxResourceMiss:value});}catch{}};
2378
+ const __forgeaxNoteHit=()=>{try{__forgeaxNativePostMessage?.({__forgeaxResourceHit:1});}catch{}};
2379
+ const __forgeaxNoteExternal=(kind,specifier)=>{const value={kind,specifier:String(specifier)};try{__forgeaxNativePostMessage?.({__forgeaxExternalRequest:value});}catch{}};
2380
+ const __forgeaxResponse=(entry)=>{__forgeaxNoteHit();return new Response(__forgeaxBytes(entry.data),{status:200,headers:{'Content-Type':entry.mime}});};
2381
+ const __forgeaxImportUrls=new Map();
2382
+ const __forgeaxImport=async(value)=>{const entry=__forgeaxLookup(value);if(entry===undefined){__forgeaxNote('import',value);throw new TypeError('forgeax single-html worker resource miss');}let url=__forgeaxImportUrls.get(entry.path);if(url===undefined){url=__forgeaxNativeURL.createObjectURL(new Blob([__forgeaxBytes(entry.data)],{type:'text/javascript'}));__forgeaxOwnedUrls.add(url);__forgeaxImportUrls.set(entry.path,url);}__forgeaxNoteHit();return import(url);};
2383
+ globalThis.__forgeaxImport=__forgeaxImport;
2384
+ if(typeof __forgeaxNativeFetch==='function')globalThis.fetch=(input,init)=>{const entry=__forgeaxLookup(input);if(entry!==undefined)return Promise.resolve(__forgeaxResponse(entry));const text=String(input);if(/^(?:data:|blob:)/i.test(text))return __forgeaxNativeFetch(input,init);if(/^https?:/i.test(text)){__forgeaxNoteExternal('fetch',input);}else{__forgeaxNote('fetch',input);}return Promise.reject(new TypeError('forgeax single-html worker resource miss'));};
2385
+ const __forgeaxWorkerPrelude=__FORGEAX_WORKER_FACTORY__;
2386
+ const __forgeaxNativeWorker=globalThis.Worker;
2387
+ if(typeof __forgeaxNativeWorker==='function')globalThis.Worker=class extends __forgeaxNativeWorker{constructor(input,options){const entry=__forgeaxLookup(input);if(entry===undefined){__forgeaxNote('worker',input);throw new TypeError('forgeax single-html worker resource miss');}const source=new TextDecoder().decode(__forgeaxBytes(entry.data));super(__forgeaxWorkerURL(__forgeaxWorkerPrelude(entry.path)+source),options);}};
2388
+ globalThis.addEventListener?.('message',(event)=>{if(event.data?.__forgeaxResourceMiss||event.data?.__forgeaxResourceHit||event.data?.__forgeaxExternalRequest){try{__forgeaxNativePostMessage?.(event.data);}catch{}event.stopImmediatePropagation?.();}});
2389
+ globalThis.addEventListener?.('unload',()=>{for(const url of __forgeaxOwnedUrls)__forgeaxNativeURL.revokeObjectURL(url);__forgeaxOwnedUrls.clear();});`;
2390
+ const factoryToken = "__FORGEAX_WORKER_FACTORY__";
2391
+ const recursiveTemplate = template;
2392
+ const factorySource = `(sourcePath)=>{const source=${JSON.stringify(recursiveTemplate)};return source.replace(${JSON.stringify(factoryToken)},()=> '('+__forgeaxWorkerPrelude.toString()+')').replace('__FORGEAX_SOURCE_PATH__',()=>JSON.stringify(sourcePath));}`;
2393
+ return `const __forgeaxWorkerPrelude=${factorySource};`;
2394
+ }
2395
+ function runtimeBootstrap(resources) {
2396
+ const workerFactory = workerPrelude(resources);
2397
+ return `(() => {
2398
+ globalThis.process ??= { env: {}, versions: { node: '0.0.0' }, platform: 'browser', argv: [] };
2399
+ const nativeURL = globalThis.URL;
2400
+ const nativeFetch = globalThis.fetch?.bind(globalThis);
2401
+ const nodes = [...document.querySelectorAll('script[data-forgeax-asset]')];
2402
+ const entries = nodes.map((node) => ({ path: node.getAttribute('data-path'), mime: node.getAttribute('data-mime') || 'application/octet-stream', data: (node.textContent || '').trim() })).filter((entry) => typeof entry.path === 'string');
2403
+ const decode = (value) => { const binary = atob(value); const bytes = new Uint8Array(binary.length); for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index); return bytes; };
2404
+ const normalize = (value) => { try { return decodeURIComponent(String(value).split(/[?#]/, 1)[0]).replaceAll('\\\\', '/').replace(/^\\/+/, ''); } catch { return undefined; } };
2405
+ const withoutBundle = (value) => String(value).replace(/^__forgeax-bundle\\//, '');
2406
+ const lookup = (input) => { let url; try { url = input instanceof nativeURL ? input : new nativeURL(String(input), document.baseURI); } catch { return undefined; } if (url.protocol === 'data:' || url.protocol === 'blob:') return undefined; const path = normalize(url.pathname); if (path === undefined) return undefined; const normalized = withoutBundle(path); const exact = entries.find((entry) => entry.path === path || withoutBundle(entry.path) === normalized); if (exact !== undefined) return exact; const suffix = entries.filter((entry) => normalized.endsWith('/' + withoutBundle(entry.path))); if (suffix.length === 1) return suffix[0]; const name = normalized.slice(normalized.lastIndexOf('/') + 1); const matches = entries.filter((entry) => withoutBundle(entry.path).slice(withoutBundle(entry.path).lastIndexOf('/') + 1) === name); return matches.length === 1 ? matches[0] : undefined; };
2407
+ const resourceMisses = [];
2408
+ const externalRequests = [];
2409
+ let resourceHits = 0;
2410
+ const noteMiss = (kind, specifier) => { resourceMisses.push({ kind, specifier: String(specifier), realm: 'main' }); };
2411
+ const noteExternal = (kind, specifier) => { externalRequests.push({ kind, specifier: String(specifier), realm: 'main' }); };
2412
+ const response = (entry) => { resourceHits += 1; return new Response(decode(entry.data), { status: 200, headers: { 'Content-Type': entry.mime } }); };
2413
+ const ownedBlobUrls = new Set();
2414
+ const makeBlobUrl = (entry) => { const url = nativeURL.createObjectURL(new Blob([decode(entry.data)], { type: entry.mime })); ownedBlobUrls.add(url); return url; };
2415
+ const dataUrl = (source) => { const bytes = new TextEncoder().encode(source); let binary = ''; for (const byte of bytes) binary += String.fromCharCode(byte); return 'data:text/javascript;base64,' + btoa(binary); };
2416
+ const workerUrl = (source) => { if (/^(?:file:|data:|about:)/i.test(String(globalThis.location?.protocol ?? ''))) return dataUrl(source); const url = nativeURL.createObjectURL(new Blob([source], { type: 'text/javascript' })); ownedBlobUrls.add(url); return url; };
2417
+ const importUrls = new Map();
2418
+ const forgeaxImport = async (specifier) => { const entry = lookup(specifier); if (entry === undefined) { const text = String(specifier); if (/^(?:https?:)/i.test(text)) noteExternal('import', text); else noteMiss('import', text); throw new TypeError('forgeax single-html resource miss'); } let url = importUrls.get(entry.path); if (url === undefined) { url = makeBlobUrl(entry); importUrls.set(entry.path, url); } resourceHits += 1; return import(url); };
2419
+ globalThis.__forgeaxImport = forgeaxImport;
2420
+ const external = (input) => { try { const url = input instanceof nativeURL ? input : new nativeURL(String(input), document.baseURI); return url.protocol === 'http:' || url.protocol === 'https:'; } catch { return false; } };
2421
+ if (typeof nativeFetch === 'function') globalThis.fetch = (input, init) => { const entry = lookup(input); if (entry !== undefined) return Promise.resolve(response(entry)); if (/^(?:data:|blob:)/i.test(String(input))) return nativeFetch(input, init); if (external(input)) { noteExternal('fetch', input); return Promise.reject(new TypeError('forgeax single-html blocked an external request')); } noteMiss('fetch', input); return Promise.reject(new TypeError('forgeax single-html resource miss')); };
2422
+ ${workerFactory}
2423
+ const nativeWorker = globalThis.Worker;
2424
+ const attachWorker = (worker) => { worker.addEventListener('message', (event) => { const miss = event.data?.__forgeaxResourceMiss; if (miss !== undefined) { resourceMisses.push({ ...miss, realm: 'worker' }); event.stopImmediatePropagation?.(); } const externalRequest = event.data?.__forgeaxExternalRequest; if (externalRequest !== undefined) { externalRequests.push({ ...externalRequest, realm: 'worker' }); event.stopImmediatePropagation?.(); } const hit = event.data?.__forgeaxResourceHit; if (typeof hit === 'number' && Number.isFinite(hit)) resourceHits += hit; }); return worker; };
2425
+ if (typeof nativeWorker === 'function') globalThis.Worker = class extends nativeWorker { constructor(input, options) { const entry = lookup(input); if (entry === undefined) { const text = String(input); if (external(input)) noteExternal('worker', text); else noteMiss('worker', text); throw new TypeError('forgeax single-html worker resource miss'); } const source = new TextDecoder().decode(decode(entry.data)); super(workerUrl(__forgeaxWorkerPrelude(entry.path) + source), options); attachWorker(this); } };
2426
+ const witness = () => ({ ready: document.documentElement.dataset.forgeaxSingleHtmlReady === 'true', resourceHits, resourceMisses: resourceMisses.slice(), externalRequests: externalRequests.slice() });
2427
+ globalThis.__forgeaxSingleHtml = { lookup, entries: entries.map(({ path, mime }) => ({ path, mime })), witness };
2428
+ globalThis.addEventListener('pagehide', () => { for (const url of ownedBlobUrls) nativeURL.revokeObjectURL(url); ownedBlobUrls.clear(); }, { once: true });
2429
+ document.documentElement.dataset.forgeaxSingleHtmlReady = 'true';
2430
+ })();`;
2431
+ }
2432
+ function escapeAttribute(value) {
2433
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2434
+ }
2435
+ function distResources(distRoot, manifest) {
2436
+ const rows = [
2437
+ ...manifest.artifacts,
2438
+ { path: "forgeax-dist.json" }
2439
+ ];
2440
+ return Promise.all(
2441
+ rows.filter((artifact) => artifact.path !== "index.html").map(async (artifact) => ({
2442
+ path: artifact.path,
2443
+ bytes: await readFile(resolve(distRoot, artifact.path)),
2444
+ mediaType: "mediaType" in artifact ? artifact.mediaType : mediaType(artifact.path)
2445
+ }))
2446
+ );
2447
+ }
2448
+ function buildHtml(indexHtml, distArtifacts, bundle) {
2449
+ const resources = /* @__PURE__ */ new Map();
2450
+ for (const resource of [...distArtifacts, ...bundle.artifacts]) {
2451
+ if (resources.has(resource.path)) {
2452
+ return errorResult(
2453
+ "single-html-asset-duplicate",
2454
+ "embedded resource paths to be unique",
2455
+ "Repair the generated bundle path collision before packaging.",
2456
+ { path: resource.path }
2457
+ );
2458
+ }
2459
+ resources.set(resource.path, resource);
2460
+ }
2461
+ const replacements = [];
2462
+ for (const element of scanHtmlElements(indexHtml)) {
2463
+ if (isModuleScript(element) || isModulePreload(element)) {
2464
+ replacements.push({ start: element.start, end: element.end, value: "" });
2465
+ continue;
2466
+ }
2467
+ if (isStylesheet(element)) {
2468
+ const href = attributeValue(element, "href");
2469
+ if (href === void 0) {
2470
+ return errorResult(
2471
+ "single-html-stylesheet-missing",
2472
+ "stylesheet link to contain href",
2473
+ "Repair the generated HTML stylesheet link."
2474
+ );
2475
+ }
2476
+ const path = resourcePath(href, "index.html");
2477
+ const stylesheet = path === void 0 ? void 0 : resourceForPath(path, resources);
2478
+ if (stylesheet === void 0) {
2479
+ return errorResult(
2480
+ "single-html-css-asset-missing",
2481
+ "stylesheet link to resolve to an embedded artifact",
2482
+ "Add the stylesheet to the dist closure and rebuild.",
2483
+ { href, path: path ?? null }
2484
+ );
2485
+ }
2486
+ const css = inlineCss(
2487
+ Buffer.from(stylesheet.bytes).toString("utf8"),
2488
+ stylesheet.path,
2489
+ resources
2490
+ );
2491
+ if (!css.ok) return css;
2492
+ replacements.push({
2493
+ start: element.start,
2494
+ end: element.end,
2495
+ value: `<style data-forgeax-inline-css>${css.value}</style>`
2496
+ });
2497
+ continue;
2498
+ }
2499
+ if (element.name === "style" && element.contentStart !== void 0 && element.contentEnd !== void 0) {
2500
+ const css = inlineCss(
2501
+ indexHtml.slice(element.contentStart, element.contentEnd),
2502
+ "index.html",
2503
+ resources
2504
+ );
2505
+ if (!css.ok) return css;
2506
+ replacements.push({ start: element.contentStart, end: element.contentEnd, value: css.value });
2507
+ continue;
2508
+ }
2509
+ const resourceAttribute = element.name === "img" || element.name === "source" || element.name === "video" || element.name === "audio" || element.name === "link" ? element.attributes.find((candidate) => ["src", "poster", "href"].includes(candidate.name)) : void 0;
2510
+ const resourceAttributeLocation = resourceAttribute === void 0 ? void 0 : element.attributeLocations[resourceAttribute.name];
2511
+ const resourceAttributeRange = resourceAttribute === void 0 || resourceAttributeLocation === void 0 ? void 0 : attributeValueRange(indexHtml, resourceAttributeLocation);
2512
+ if (resourceAttribute?.value !== void 0 && resourceAttributeRange !== void 0 && !/^(?:data:|blob:|#)/i.test(resourceAttribute.value)) {
2513
+ const path = resourcePath(resourceAttribute.value, "index.html");
2514
+ const embedded = path === void 0 ? void 0 : resourceForPath(path, resources);
2515
+ if (embedded === void 0) {
2516
+ return errorResult(
2517
+ "single-html-asset-missing",
2518
+ "every local HTML resource to resolve to an embedded dist artifact",
2519
+ "Add the referenced asset to the project closure and rebuild.",
2520
+ { value: resourceAttribute.value, path: path ?? null }
2521
+ );
2522
+ }
2523
+ replacements.push({
2524
+ start: resourceAttributeRange.startOffset,
2525
+ end: resourceAttributeRange.endOffset,
2526
+ value: dataUri(embedded.bytes, embedded.mediaType)
2527
+ });
2528
+ continue;
2529
+ }
2530
+ if (element.name === "script" && attributeValue(element, "src") !== void 0) {
2531
+ return errorResult(
2532
+ "single-html-script-external",
2533
+ "non-module script sources to be absent from the generated host",
2534
+ "Move the script into the production module entry before packaging.",
2535
+ { src: attributeValue(element, "src") }
2536
+ );
2537
+ }
2538
+ }
2539
+ const assetNodes = [...resources.values()].sort((left, right) => left.path.localeCompare(right.path)).map(
2540
+ (resource, index) => `<script type="application/octet-stream" id="forgeax-asset-${index}" data-forgeax-asset data-path="${escapeAttribute(resource.path)}" data-mime="${escapeAttribute(resource.mediaType)}">${Buffer.from(resource.bytes).toString("base64")}</script>`
2541
+ ).join("");
2542
+ const bootstrap = `<script type="application/javascript">${safeScriptText(runtimeBootstrap([...resources.values()]))}</script>`;
2543
+ const entry = `<script type="module">${safeScriptText(bundle.entrySource)}</script>`;
2544
+ const withResources = applyReplacements(indexHtml, replacements);
2545
+ const body = `${assetNodes}${bootstrap}${entry}`;
2546
+ const html = withResources.includes("</body>") ? withResources.replace(/<\/body>/i, () => `${body}</body>`) : `${withResources}${body}`;
2547
+ return { ok: true, value: { html, embeddedAssets: resources.size } };
2548
+ }
2549
+ async function writeSingleHtml(options) {
2550
+ const distRoot = resolve(options.distRoot);
2551
+ const output = resolve(options.output);
2552
+ const checksumPath = `${output}.sha256`;
2553
+ const temporary = `${output}.partial-${process.pid}`;
2554
+ const temporaryChecksum = `${checksumPath}.partial-${process.pid}`;
2555
+ try {
2556
+ const indexHtml = await readFile(resolve(distRoot, "index.html"), "utf8");
2557
+ const resources = await distResources(distRoot, options.manifest);
2558
+ const built = buildHtml(indexHtml, resources, options.bundle);
2559
+ if (!built.ok) return built;
2560
+ const bytes = Buffer.from(built.value.html, "utf8");
2561
+ const sha2562 = hashBytes(bytes);
2562
+ const manifestBytes = await readFile(resolve(distRoot, "forgeax-dist.json"));
2563
+ await mkdir(dirname(output), { recursive: true });
2564
+ await writeFile(temporary, bytes);
2565
+ await writeFile(temporaryChecksum, `${sha2562} ${basename(output)}
2566
+ `, "utf8");
2567
+ await rename(temporaryChecksum, checksumPath);
2568
+ await rename(temporary, output);
2569
+ return {
2570
+ ok: true,
2571
+ value: {
2572
+ schemaVersion: "1.0.0",
2573
+ format: SINGLE_HTML_FORMAT,
2574
+ target: "file",
2575
+ project: options.manifest.project,
2576
+ base: options.manifest.base,
2577
+ html: { path: output, bytes: bytes.byteLength, sha256: sha2562 },
2578
+ checksumPath,
2579
+ distManifestSha256: hashBytes(manifestBytes),
2580
+ embeddedAssets: built.value.embeddedAssets,
2581
+ run: {
2582
+ local: pathToFileURL(output).href,
2583
+ shared: "send the HTML file and open it in a desktop Chrome with WebGPU support"
2584
+ }
2585
+ }
2586
+ };
2587
+ } catch (cause) {
2588
+ return toErrorResult(
2589
+ cause,
2590
+ "single-html-write-failed",
2591
+ "the single HTML candidate and adjacent SHA-256 to be written atomically",
2592
+ "Repair the dist closure or output directory, then retry packaging."
2593
+ );
2594
+ } finally {
2595
+ await Promise.all([rm(temporary, { force: true }), rm(temporaryChecksum, { force: true })]);
1403
2596
  }
1404
- return {
1405
- code: fallbackCode,
1406
- expected: "the ForgeaX command to complete",
1407
- hint: "Inspect the underlying diagnostic and repair the owning input.",
1408
- detail: { reason: cause instanceof Error ? cause.message : String(cause) }
1409
- };
1410
2597
  }
1411
- var init_project = __esm({
1412
- "src/project.ts"() {
2598
+ function packageFormatError(format) {
2599
+ return errorResult(
2600
+ "package-format-unsupported",
2601
+ "package format to be web-zip or single-html",
2602
+ "Use web-zip for HTTPS hosting or single-html for a self-contained file:// delivery.",
2603
+ { format }
2604
+ );
2605
+ }
2606
+ function packageOutputError(output, format) {
2607
+ const expectedSuffix = format === "single-html" ? ".html" : ".zip";
2608
+ return errorResult(
2609
+ "package-output-suffix-mismatch",
2610
+ `package output to end with ${expectedSuffix}`,
2611
+ `Use a ${expectedSuffix} output path for ${format}.`,
2612
+ { output, format }
2613
+ );
2614
+ }
2615
+ var SINGLE_HTML_FORMAT, GENERATED_PREFIX, SingleHtmlError;
2616
+ var init_single_html = __esm({
2617
+ "src/single-html.ts"() {
2618
+ init_dist();
2619
+ init_host();
2620
+ SINGLE_HTML_FORMAT = "forgeax-single-html-game";
2621
+ GENERATED_PREFIX = "__forgeax-bundle/";
2622
+ SingleHtmlError = class extends Error {
2623
+ constructor(code, expected, hint, detail) {
2624
+ super(`${code}: ${hint}`);
2625
+ this.code = code;
2626
+ this.expected = expected;
2627
+ this.hint = hint;
2628
+ this.detail = detail;
2629
+ this.name = "SingleHtmlError";
2630
+ }
2631
+ code;
2632
+ expected;
2633
+ hint;
2634
+ detail;
2635
+ };
1413
2636
  }
1414
2637
  });
1415
2638
 
@@ -1679,6 +2902,7 @@ var init_package = __esm({
1679
2902
  "@forgeax/engine-project": "workspace:*",
1680
2903
  "@forgeax/engine-rhi-debug": "workspace:*",
1681
2904
  "@forgeax/engine-rhi-null": "workspace:*",
2905
+ "@forgeax/engine-rhi-webgpu": "workspace:*",
1682
2906
  "@forgeax/engine-render": "workspace:*",
1683
2907
  "@forgeax/engine-render-graph": "workspace:*",
1684
2908
  "@forgeax/engine-runtime": "workspace:*",
@@ -1689,11 +2913,14 @@ var init_package = __esm({
1689
2913
  "@forgeax/engine-types": "workspace:*",
1690
2914
  "@forgeax/engine-vfx-compiler": "workspace:*",
1691
2915
  "@forgeax/engine-vite-plugin-pack": "workspace:*",
2916
+ "@forgeax/engine-vite-plugin-rhi-debug": "workspace:*",
1692
2917
  "@forgeax/engine-vite-plugin-shader": "workspace:*",
1693
2918
  jiti: "1.21.7",
2919
+ parse5: "7.3.0",
1694
2920
  playwright: "1.60.0",
1695
2921
  vite: "8.0.10",
1696
- vitest: "4.1.5"
2922
+ vitest: "4.1.11",
2923
+ webgpu: "^0.4.0"
1697
2924
  },
1698
2925
  devDependencies: {
1699
2926
  "@types/node": "^20.14.0"
@@ -1867,7 +3094,7 @@ var init_init = __esm({
1867
3094
  "@webgpu/types": "0.1.71",
1868
3095
  tsx: "4.23.1",
1869
3096
  typescript: "6.0.3",
1870
- vitest: "4.1.5"
3097
+ vitest: "4.1.11"
1871
3098
  };
1872
3099
  }
1873
3100
  });
@@ -1889,15 +3116,12 @@ async function findSdkContext() {
1889
3116
  const templates = new Map(
1890
3117
  manifest.templates.map((template) => [template.id, resolve(cursor, template.root)])
1891
3118
  );
1892
- const defaultTemplate = manifest.templates.find((template) => template.default);
1893
- if (defaultTemplate === void 0) throw new Error("sdk-default-template-missing");
1894
3119
  const store = resolve(cursor, "store", "pnpm");
1895
3120
  return {
1896
3121
  root: cursor,
1897
3122
  manifest,
1898
3123
  ...await readable(store) ? { store } : {},
1899
- templates,
1900
- defaultTemplate: defaultTemplate.id
3124
+ templates
1901
3125
  };
1902
3126
  }
1903
3127
  const parent = dirname(cursor);
@@ -1917,10 +3141,14 @@ function agentOnboarding(sdk, projectRoot2) {
1917
3141
  resolve(root, "skills", "forgeax-engine-sdk", "SKILL.md"),
1918
3142
  resolve(root, "skills", "forgeax-engine-sdk", "references", "feature-catalog.md")
1919
3143
  ],
1920
- next: projectRoot2 === void 0 ? {
1921
- cwd: root,
1922
- argv: ["node", "./bin/forgeax.mjs", "new", "../my-game"]
1923
- } : { cwd: root, argv: ["pnpm", "exec", "forgeax", "list", "--json"] }
3144
+ ...projectRoot2 === void 0 ? {
3145
+ templateSelection: {
3146
+ required: true,
3147
+ game3d: "game-3d",
3148
+ otherwise: "empty"
3149
+ }
3150
+ } : {},
3151
+ ...projectRoot2 === void 0 ? {} : { next: { cwd: root, argv: ["pnpm", "exec", "forgeax", "list", "--json"] } }
1924
3152
  };
1925
3153
  }
1926
3154
  function sdkInitPath(sdk) {
@@ -1934,6 +3162,7 @@ function sdkProjectInstallArgs(store) {
1934
3162
  "install",
1935
3163
  "--frozen-lockfile",
1936
3164
  "--ignore-scripts",
3165
+ "--config.pm-on-fail=ignore",
1937
3166
  "--side-effects-cache=true",
1938
3167
  "--child-concurrency=1"
1939
3168
  ];
@@ -1943,6 +3172,7 @@ function sdkBootstrapInstallArgs(store) {
1943
3172
  const common = [
1944
3173
  "install",
1945
3174
  "--frozen-lockfile",
3175
+ "--config.pm-on-fail=ignore",
1946
3176
  "--child-concurrency=1",
1947
3177
  "--side-effects-cache=true"
1948
3178
  ];
@@ -2000,8 +3230,8 @@ async function requireSdkInitialization(sdk) {
2000
3230
  return { ok: true, value: state };
2001
3231
  }
2002
3232
  async function copyBootstrapInputs(sdk, root) {
2003
- const template = sdk.templates.get(sdk.defaultTemplate);
2004
- if (template === void 0) throw new Error("sdk-default-template-missing");
3233
+ const template = sdk.templates.get("empty");
3234
+ if (template === void 0) throw new Error("sdk-bootstrap-template-missing");
2005
3235
  for (const name of ["package.json", "pnpm-lock.yaml", "pnpm-workspace.yaml"]) {
2006
3236
  await cp(resolve(template, name), resolve(root, name));
2007
3237
  }
@@ -2193,13 +3423,13 @@ async function pathKind(path) {
2193
3423
  throw cause;
2194
3424
  }
2195
3425
  }
2196
- async function filesUnder2(root, directory = root) {
3426
+ async function filesUnder3(root, directory = root) {
2197
3427
  const files = [];
2198
3428
  for (const name of (await readdir(directory)).sort()) {
2199
3429
  const path = resolve(directory, name);
2200
3430
  const info = await lstat(path);
2201
3431
  if (info.isSymbolicLink()) throw new Error(`skill-source-symlink: ${path}`);
2202
- if (info.isDirectory()) files.push(...await filesUnder2(root, path));
3432
+ if (info.isDirectory()) files.push(...await filesUnder3(root, path));
2203
3433
  else if (info.isFile()) files.push(path);
2204
3434
  }
2205
3435
  return files;
@@ -2211,7 +3441,7 @@ async function skillRow(sourceRoot, id) {
2211
3441
  if (await pathKind(resolve(root, "SKILL.md")) !== "file") {
2212
3442
  throw new Error(`skill-entry-missing: ${id}/SKILL.md`);
2213
3443
  }
2214
- const files = await filesUnder2(root);
3444
+ const files = await filesUnder3(root);
2215
3445
  return {
2216
3446
  id,
2217
3447
  root: `skills/${id}`,
@@ -2228,8 +3458,8 @@ async function discoverProjectSkills(root) {
2228
3458
  return Promise.all(ids.map((id) => skillRow(sourceRoot, id)));
2229
3459
  }
2230
3460
  async function sameFiles(leftRoot, rightRoot) {
2231
- const left = await filesUnder2(leftRoot);
2232
- const right = await filesUnder2(rightRoot);
3461
+ const left = await filesUnder3(leftRoot);
3462
+ const right = await filesUnder3(rightRoot);
2233
3463
  const leftNames = left.map((path) => slash(relative(leftRoot, path)));
2234
3464
  const rightNames = right.map((path) => slash(relative(rightRoot, path)));
2235
3465
  if (JSON.stringify(leftNames) !== JSON.stringify(rightNames)) return false;
@@ -2653,8 +3883,8 @@ async function initCommand(options = {}) {
2653
3883
  const applied = await applyInitPlan(facts.value, plan.value, options);
2654
3884
  if (!applied.ok || options.dryRun === true) return applied;
2655
3885
  if (sdk !== void 0) {
2656
- const template = sdk.templates.get(sdk.defaultTemplate);
2657
- if (template === void 0) throw new Error("sdk-default-template-missing");
3886
+ const template = sdk.templates.get("empty");
3887
+ if (template === void 0) throw new Error("sdk-bootstrap-template-missing");
2658
3888
  await copyFile(
2659
3889
  resolve(template, "pnpm-lock.yaml"),
2660
3890
  resolve(facts.value.root, "pnpm-lock.yaml")
@@ -2722,7 +3952,18 @@ async function newCommand(options = {}) {
2722
3952
  }
2723
3953
  };
2724
3954
  }
2725
- const templateId = options.template ?? sdk.defaultTemplate;
3955
+ if (options.template === void 0) {
3956
+ return {
3957
+ ok: false,
3958
+ error: {
3959
+ code: "sdk-template-required",
3960
+ expected: "forgeax new to select exactly one template with --template",
3961
+ hint: "Use --template game-3d for a 3D game; otherwise use --template empty.",
3962
+ detail: { templates: [...sdk.templates.keys()].sort() }
3963
+ }
3964
+ };
3965
+ }
3966
+ const templateId = options.template;
2726
3967
  const template = sdk.templates.get(templateId);
2727
3968
  if (template === void 0) {
2728
3969
  return {
@@ -2854,7 +4095,7 @@ __export(plugin_authoring_exports, {
2854
4095
  pluginInstallCommand: () => pluginInstallCommand,
2855
4096
  pluginUninstallCommand: () => pluginUninstallCommand
2856
4097
  });
2857
- async function readManifest(root) {
4098
+ async function readManifest2(root) {
2858
4099
  const path = resolve(root, "forge.json");
2859
4100
  try {
2860
4101
  const raw = await readFile(path, "utf8");
@@ -2907,7 +4148,7 @@ async function mutateDependency(root, action, dependency) {
2907
4148
  }
2908
4149
  async function pluginInstallCommand(options) {
2909
4150
  const root = resolve(options.root ?? process.cwd());
2910
- const manifest = await readManifest(root);
4151
+ const manifest = await readManifest2(root);
2911
4152
  if (!manifest.ok) return manifest;
2912
4153
  const parsed = GameProjectSchema.safeParse(manifest.value.value);
2913
4154
  if (!parsed.success) {
@@ -2960,7 +4201,7 @@ async function pluginInstallCommand(options) {
2960
4201
  }
2961
4202
  async function pluginUninstallCommand(options) {
2962
4203
  const root = resolve(options.root ?? process.cwd());
2963
- const manifest = await readManifest(root);
4204
+ const manifest = await readManifest2(root);
2964
4205
  if (!manifest.ok) return manifest;
2965
4206
  const parsed = GameProjectSchema.safeParse(manifest.value.value);
2966
4207
  if (!parsed.success) {
@@ -3048,26 +4289,62 @@ function createCliRhiDebugOperationContext() {
3048
4289
  };
3049
4290
  }
3050
4291
  },
3051
- async createReplayBackend() {
4292
+ async createReplayBackend(tape) {
4293
+ let createDawn;
4294
+ let gpuGlobals;
4295
+ try {
4296
+ const dawn = await import('webgpu');
4297
+ createDawn = dawn.create;
4298
+ gpuGlobals = dawn.globals;
4299
+ } catch (cause) {
4300
+ return {
4301
+ ok: false,
4302
+ error: {
4303
+ code: "replay-backend-unavailable",
4304
+ expected: "the Dawn WebGPU provider to load",
4305
+ hint: "Install the DevKit runtime closure and retry rhi.inspect.",
4306
+ detail: {
4307
+ stage: "provider",
4308
+ cause: cause instanceof Error ? cause.message : String(cause)
4309
+ }
4310
+ }
4311
+ };
4312
+ }
4313
+ Object.assign(globalThis, gpuGlobals);
4314
+ const gpu = createDawn([]);
4315
+ if (!("navigator" in globalThis) || globalThis.navigator === void 0) {
4316
+ Object.defineProperty(globalThis, "navigator", {
4317
+ value: {},
4318
+ configurable: true,
4319
+ writable: true
4320
+ });
4321
+ }
4322
+ Object.defineProperty(globalThis.navigator, "gpu", {
4323
+ value: gpu,
4324
+ configurable: true,
4325
+ writable: true
4326
+ });
3052
4327
  const adapter = await rhi.requestAdapter();
3053
4328
  if (!adapter.ok) {
3054
4329
  return {
3055
4330
  ok: false,
3056
4331
  error: {
3057
4332
  code: "replay-backend-unavailable",
3058
- expected: "a fresh rhi-null adapter",
4333
+ expected: "a fresh Dawn WebGPU adapter",
3059
4334
  hint: adapter.error.hint,
3060
4335
  detail: { stage: "adapter", cause: adapter.error.hint }
3061
4336
  }
3062
4337
  };
3063
4338
  }
3064
- const device = await adapter.value.requestDevice();
4339
+ const device = await adapter.value.requestDevice(
4340
+ replayDeviceRequest(tape, adapter.value.features, adapter.value.limits)
4341
+ );
3065
4342
  if (!device.ok) {
3066
4343
  return {
3067
4344
  ok: false,
3068
4345
  error: {
3069
4346
  code: "replay-backend-unavailable",
3070
- expected: "a fresh rhi-null device",
4347
+ expected: "a fresh Dawn WebGPU device satisfying the recorded tape",
3071
4348
  hint: device.error.hint,
3072
4349
  detail: { stage: "device", cause: device.error.hint }
3073
4350
  }
@@ -3105,6 +4382,9 @@ function renderRhiDebugHelp() {
3105
4382
  return [
3106
4383
  "forgeax run <operation>",
3107
4384
  ...discoverRhiDebugOperations().map((operation) => ` ${operation.name}: ${operation.summary}`),
4385
+ "Usage:",
4386
+ " forgeax run rhi.summary --artifact PATH --digest SHA256 --json",
4387
+ " forgeax run rhi.inspect --artifact PATH --digest SHA256 --work-index N --fields pipeline,bindings,pixels --json",
3108
4388
  "ArtifactRef schema:",
3109
4389
  JSON.stringify(RHI_DEBUG_OPERATION_MANIFEST.artifactRefSchema)
3110
4390
  ].join("\n");
@@ -3232,7 +4512,7 @@ async function runRhiDebugOperation(name, input, context) {
3232
4512
  "Provide a fresh device and shader factory before running rhi.inspect."
3233
4513
  );
3234
4514
  }
3235
- const backend = await context.createReplayBackend();
4515
+ const backend = await context.createReplayBackend(decoded.value);
3236
4516
  if (!backend.ok) return backend;
3237
4517
  const opened = await openReplay(decoded.value, backend.value);
3238
4518
  if (!opened.ok) return { ok: false, error: coreError(opened.error) };
@@ -3324,7 +4604,10 @@ var init_operations = __esm({
3324
4604
  properties: {
3325
4605
  artifact: artifactRefSchema,
3326
4606
  workIndex: { type: "integer", minimum: 0 },
3327
- fields: { type: "array", items: { type: "string" } }
4607
+ fields: {
4608
+ type: "array",
4609
+ items: { type: "string", enum: ["bindings", "pipeline", "pixels"] }
4610
+ }
3328
4611
  },
3329
4612
  required: ["artifact", "workIndex"],
3330
4613
  additionalProperties: false
@@ -3364,113 +4647,840 @@ async function sdkInstallCommand(options) {
3364
4647
  targetExists = false;
3365
4648
  entries2 = [];
3366
4649
  }
3367
- if (entries2.length > 0) {
3368
- return {
3369
- ok: false,
3370
- error: {
3371
- code: "sdk-target-not-empty",
3372
- expected: "the SDK install target to be absent or empty",
3373
- hint: "Choose an empty versioned SDK directory so existing files cannot be overwritten.",
3374
- detail: { root }
3375
- }
3376
- };
4650
+ if (entries2.length > 0) {
4651
+ return {
4652
+ ok: false,
4653
+ error: {
4654
+ code: "sdk-target-not-empty",
4655
+ expected: "the SDK install target to be absent or empty",
4656
+ hint: "Choose an empty versioned SDK directory so existing files cannot be overwritten.",
4657
+ detail: { root }
4658
+ }
4659
+ };
4660
+ }
4661
+ const version = options.version ?? await packageVersion();
4662
+ const parent = dirname(root);
4663
+ await mkdir(parent, { recursive: true });
4664
+ let staging;
4665
+ let download;
4666
+ try {
4667
+ staging = await mkdtemp(resolve(parent, `.${basename(root)}.forgeax-sdk-staging-`));
4668
+ download = await mkdtemp(resolve(parent, `.${basename(root)}.forgeax-sdk-download-`));
4669
+ await execFileAsync4(
4670
+ process.env.FORGEAX_NPM_CLIENT ?? "npm",
4671
+ [
4672
+ "install",
4673
+ "--prefix",
4674
+ download,
4675
+ "--ignore-scripts",
4676
+ "--no-package-lock",
4677
+ "--no-save",
4678
+ "--fund=false",
4679
+ "--audit=false",
4680
+ `@forgeax/engine-sdk@${version}`
4681
+ ],
4682
+ { env: { ...process.env, CI: "true" }, maxBuffer: 16 * 1024 * 1024 }
4683
+ );
4684
+ const carrier = resolve(download, "node_modules", "@forgeax", "engine-sdk", "sdk");
4685
+ const manifest = JSON.parse(await readFile(resolve(carrier, "sdk-manifest.json"), "utf8"));
4686
+ if (manifest.sdkVersion !== version) throw new Error("sdk-carrier-version-mismatch");
4687
+ for (const name of await readdir(carrier)) {
4688
+ await cp(resolve(carrier, name), resolve(staging, name), {
4689
+ recursive: true,
4690
+ errorOnExist: true,
4691
+ force: false
4692
+ });
4693
+ }
4694
+ if (!targetExists) {
4695
+ await rename(staging, root);
4696
+ staging = void 0;
4697
+ } else {
4698
+ for (const name of await readdir(staging)) {
4699
+ await rename(resolve(staging, name), resolve(root, name));
4700
+ }
4701
+ }
4702
+ return {
4703
+ ok: true,
4704
+ value: {
4705
+ root,
4706
+ sdkVersion: version,
4707
+ source: "@forgeax/engine-sdk",
4708
+ next: { cwd: root, argv: ["node", "./bin/forgeax.mjs", "init"] }
4709
+ }
4710
+ };
4711
+ } catch (cause) {
4712
+ return { ok: false, error: commandError(cause, "sdk-install-failed") };
4713
+ } finally {
4714
+ if (staging !== void 0) await rm(staging, { recursive: true, force: true });
4715
+ if (download !== void 0) await rm(download, { recursive: true, force: true });
4716
+ }
4717
+ }
4718
+ var execFileAsync4;
4719
+ var init_sdk_install = __esm({
4720
+ "src/sdk-install.ts"() {
4721
+ init_project();
4722
+ execFileAsync4 = promisify(execFile);
4723
+ }
4724
+ });
4725
+ async function shaderCheckCommand(options = {}) {
4726
+ const facts = await readProjectFacts(options.root);
4727
+ if (!facts.ok) return facts;
4728
+ const target = options.path === void 0 ? void 0 : resolve(facts.value.root, options.path);
4729
+ if (target !== void 0) {
4730
+ try {
4731
+ await readFile(target);
4732
+ } catch (cause) {
4733
+ return { ok: false, error: commandError(cause, "shader-source-unreadable") };
4734
+ }
4735
+ }
4736
+ const temporary = await mkdtemp(resolve(tmpdir(), "forgeax-shader-check-"));
4737
+ const previous = process.cwd();
4738
+ try {
4739
+ process.chdir(facts.value.root);
4740
+ const config = await createViteConfig(facts.value, "build");
4741
+ await build({
4742
+ ...config,
4743
+ build: { ...config.build, outDir: temporary, emptyOutDir: true, write: true }
4744
+ });
4745
+ return { ok: true, value: { root: facts.value.root, path: target ?? null } };
4746
+ } catch (cause) {
4747
+ return { ok: false, error: commandError(cause, "shader-check-failed") };
4748
+ } finally {
4749
+ process.chdir(previous);
4750
+ await rm(temporary, { recursive: true, force: true });
4751
+ }
4752
+ }
4753
+ var init_shader_check = __esm({
4754
+ "src/shader-check.ts"() {
4755
+ init_host();
4756
+ init_project();
4757
+ }
4758
+ });
4759
+ function fail(code, expected, hint, detail = {}) {
4760
+ throw new SoftwareCaptureError(code, expected, hint, detail);
4761
+ }
4762
+ async function pathExists2(path) {
4763
+ try {
4764
+ await access(path);
4765
+ return true;
4766
+ } catch {
4767
+ return false;
4768
+ }
4769
+ }
4770
+ async function reservePort() {
4771
+ const reservation = createServer();
4772
+ await new Promise((resolveListen, rejectListen) => {
4773
+ reservation.once("error", rejectListen);
4774
+ reservation.listen(0, "127.0.0.1", resolveListen);
4775
+ });
4776
+ const address = reservation.address();
4777
+ const port = typeof address === "object" && address !== null ? address.port : 0;
4778
+ await new Promise((resolveClose, rejectClose) => {
4779
+ reservation.close((error) => error === void 0 ? resolveClose() : rejectClose(error));
4780
+ });
4781
+ if (port === 0) throw new Error("OS did not assign an ephemeral capture port");
4782
+ return port;
4783
+ }
4784
+ async function stopProcess(child) {
4785
+ if (child.exitCode !== null || child.signalCode !== null) return;
4786
+ await new Promise((resolveStop) => {
4787
+ const timeout = setTimeout(() => {
4788
+ child.kill("SIGKILL");
4789
+ resolveStop();
4790
+ }, 2e3);
4791
+ child.once("exit", () => {
4792
+ clearTimeout(timeout);
4793
+ resolveStop();
4794
+ });
4795
+ child.kill("SIGTERM");
4796
+ });
4797
+ }
4798
+ async function startVirtualDisplay(width, height) {
4799
+ if (process.platform !== "linux" || process.env.DISPLAY !== void 0) {
4800
+ return {
4801
+ ...process.env.DISPLAY === void 0 ? {} : { value: process.env.DISPLAY },
4802
+ async close() {
4803
+ }
4804
+ };
4805
+ }
4806
+ for (let offset = 0; offset < 100; offset += 1) {
4807
+ const number = 90 + (process.pid + offset) % 100;
4808
+ const socket = `/tmp/.X11-unix/X${number}`;
4809
+ const lock = `/tmp/.X${number}-lock`;
4810
+ if (await pathExists2(socket) || await pathExists2(lock)) continue;
4811
+ const display = `:${number}`;
4812
+ const child = spawn(
4813
+ "Xvfb",
4814
+ [display, "-screen", "0", `${width}x${height}x24`, "-nolisten", "tcp"],
4815
+ { stdio: ["ignore", "ignore", "pipe"] }
4816
+ );
4817
+ let diagnostic = "";
4818
+ child.stderr?.setEncoding("utf8");
4819
+ child.stderr?.on("data", (chunk) => {
4820
+ diagnostic += chunk;
4821
+ });
4822
+ try {
4823
+ await new Promise((resolveReady, rejectReady) => {
4824
+ const timeout = setTimeout(
4825
+ () => rejectReady(new Error(`Xvfb did not create ${socket}: ${diagnostic.trim()}`)),
4826
+ 5e3
4827
+ );
4828
+ const poll = setInterval(() => {
4829
+ void pathExists2(socket).then((exists) => {
4830
+ if (!exists) return;
4831
+ clearTimeout(timeout);
4832
+ clearInterval(poll);
4833
+ resolveReady();
4834
+ });
4835
+ }, 50);
4836
+ child.once("error", (error) => {
4837
+ clearTimeout(timeout);
4838
+ clearInterval(poll);
4839
+ rejectReady(error);
4840
+ });
4841
+ child.once("exit", (code, signal) => {
4842
+ clearTimeout(timeout);
4843
+ clearInterval(poll);
4844
+ rejectReady(
4845
+ new Error(`Xvfb exited before ready (${code ?? signal}): ${diagnostic.trim()}`)
4846
+ );
4847
+ });
4848
+ });
4849
+ return { value: display, close: () => stopProcess(child) };
4850
+ } catch (cause) {
4851
+ await stopProcess(child);
4852
+ throw cause;
4853
+ }
4854
+ }
4855
+ throw new Error("no free X11 display number was available for browser capture");
4856
+ }
4857
+ async function lavapipeIcd() {
4858
+ for (const candidate of [
4859
+ "/usr/share/vulkan/icd.d/lvp_icd.x86_64.json",
4860
+ "/usr/share/vulkan/icd.d/lvp_icd.aarch64.json"
4861
+ ]) {
4862
+ if (await pathExists2(candidate)) return candidate;
4863
+ }
4864
+ return void 0;
4865
+ }
4866
+ function evidencePath(output) {
4867
+ return output.toLowerCase().endsWith(".png") ? `${output.slice(0, -4)}.json` : `${output}.json`;
4868
+ }
4869
+ function newRunId() {
4870
+ return `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
4871
+ }
4872
+ function checkpointSlug(checkpoint) {
4873
+ const value = (checkpoint ?? "capture").trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
4874
+ return value.length === 0 ? "capture" : value;
4875
+ }
4876
+ function captureDigest(bytes) {
4877
+ return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
4878
+ }
4879
+ function observedBackend(runtime) {
4880
+ if (runtime.adapter === null) return "unknown";
4881
+ return isSoftwareAdapter(runtime) ? "software" : "hardware";
4882
+ }
4883
+ async function resolveBrowserExecutable(requested) {
4884
+ if (requested !== void 0 && requested.length > 0) {
4885
+ return await pathExists2(requested) ? requested : void 0;
4886
+ }
4887
+ const candidates = [
4888
+ process.env.FORGEAX_BROWSER_EXECUTABLE,
4889
+ "/opt/google/chrome-beta/chrome",
4890
+ "/usr/bin/google-chrome",
4891
+ "/usr/bin/google-chrome-stable",
4892
+ "/usr/bin/chromium",
4893
+ "/usr/bin/chromium-browser",
4894
+ typeof chromium.executablePath === "function" ? chromium.executablePath() : void 0
4895
+ ].filter((candidate) => candidate !== void 0 && candidate.length > 0);
4896
+ for (const candidate of candidates) {
4897
+ if (await pathExists2(candidate)) return candidate;
4898
+ }
4899
+ return void 0;
4900
+ }
4901
+ function browserLaunchArgs(backend, profile) {
4902
+ const common = ["--force-color-profile=srgb", "--force-device-scale-factor=1"];
4903
+ if (profile === "development") {
4904
+ common.unshift(
4905
+ "--enable-unsafe-webgpu",
4906
+ "--ignore-gpu-blocklist",
4907
+ "--disable-gpu-driver-bug-workarounds"
4908
+ );
4909
+ }
4910
+ if (backend !== "software") return common;
4911
+ return [
4912
+ ...common,
4913
+ "--enable-features=Vulkan",
4914
+ "--use-vulkan=swiftshader",
4915
+ "--use-angle=swiftshader",
4916
+ "--disable-vulkan-surface"
4917
+ ];
4918
+ }
4919
+ function summarizeCapturePixels(rgba, width, height) {
4920
+ const pixelCount = width * height;
4921
+ const stride = Math.max(1, Math.floor(pixelCount / 4096));
4922
+ const histogram = new Uint32Array(16);
4923
+ let lumaMin = 255;
4924
+ let lumaMax = 0;
4925
+ let sampledPixels = 0;
4926
+ for (let pixel = 0; pixel < pixelCount; pixel += stride) {
4927
+ const offset = pixel * 4;
4928
+ const red = rgba[offset];
4929
+ const green = rgba[offset + 1];
4930
+ const blue = rgba[offset + 2];
4931
+ if (red === void 0 || green === void 0 || blue === void 0) break;
4932
+ const luma = Math.round((54 * red + 183 * green + 19 * blue) / 256);
4933
+ lumaMin = Math.min(lumaMin, luma);
4934
+ lumaMax = Math.max(lumaMax, luma);
4935
+ const bucket = Math.min(15, Math.floor(luma / 16));
4936
+ histogram[bucket] = (histogram[bucket] ?? 0) + 1;
4937
+ sampledPixels += 1;
4938
+ }
4939
+ const dominantPixels = histogram.reduce((largest, count) => Math.max(largest, count), 0);
4940
+ const varyingPixels = sampledPixels - dominantPixels;
4941
+ const lumaRange = lumaMax - lumaMin;
4942
+ const requiredVariation = Math.min(sampledPixels, Math.max(8, Math.ceil(sampledPixels * 2e-3)));
4943
+ return {
4944
+ width,
4945
+ height,
4946
+ sampledPixels,
4947
+ lumaMin,
4948
+ lumaMax,
4949
+ lumaRange,
4950
+ varyingPixels,
4951
+ rendered: lumaRange >= 8 && varyingPixels >= requiredVariation
4952
+ };
4953
+ }
4954
+ function inspectCapturePng(png) {
4955
+ const decoded = parseImage(png, "image/png", { mipmap: false });
4956
+ if (!decoded.ok) throw decoded.error;
4957
+ return summarizeCapturePixels(decoded.value.bytes, decoded.value.width, decoded.value.height);
4958
+ }
4959
+ async function screenshotWithWitness(page) {
4960
+ const canvasPng = await page.locator("canvas").first().screenshot({
4961
+ type: "png",
4962
+ style: "* { visibility: hidden !important; } canvas { visibility: visible !important; }"
4963
+ });
4964
+ const png = await page.screenshot({ type: "png", caret: "hide" });
4965
+ return { png, pixels: inspectCapturePng(canvasPng) };
4966
+ }
4967
+ async function waitForCompositor(page) {
4968
+ await page.evaluate(async () => {
4969
+ await document.fonts.ready;
4970
+ await new Promise((resolveFrame) => requestAnimationFrame(() => resolveFrame()));
4971
+ await new Promise((resolveFrame) => requestAnimationFrame(() => resolveFrame()));
4972
+ });
4973
+ }
4974
+ async function runtimeWitness(page) {
4975
+ return page.evaluate(async (datasetKey) => {
4976
+ const canvas = document.querySelector("canvas");
4977
+ const uiRoot = document.querySelector("#game-ui");
4978
+ const shadowHosts = [...uiRoot?.querySelectorAll("*") ?? []].filter(
4979
+ (element) => element.shadowRoot?.childElementCount !== 0
4980
+ );
4981
+ let adapter = null;
4982
+ let adapterError = null;
4983
+ try {
4984
+ const gpuAdapter = await navigator.gpu?.requestAdapter();
4985
+ if (gpuAdapter === null || gpuAdapter === void 0) {
4986
+ adapterError = "navigator.gpu.requestAdapter() returned null";
4987
+ } else {
4988
+ const info = gpuAdapter.info;
4989
+ adapter = {
4990
+ vendor: info.vendor,
4991
+ architecture: info.architecture,
4992
+ device: info.device,
4993
+ description: info.description
4994
+ };
4995
+ }
4996
+ } catch (cause) {
4997
+ adapterError = String(cause);
4998
+ }
4999
+ const frameId = Number(document.documentElement.dataset[datasetKey]);
5000
+ const singleHtmlState = globalThis.__forgeaxSingleHtml;
5001
+ return {
5002
+ title: document.title,
5003
+ canvas: canvas instanceof HTMLCanvasElement ? { width: canvas.width, height: canvas.height } : null,
5004
+ domUi: {
5005
+ rootChildren: uiRoot?.childElementCount ?? 0,
5006
+ openShadowRoots: shadowHosts.length,
5007
+ textWitness: shadowHosts.map((host) => host.shadowRoot?.textContent?.replace(/\s+/g, " ").trim() ?? "").filter((text) => text.length > 0).join(" | ").slice(0, 500)
5008
+ },
5009
+ adapter,
5010
+ adapterError,
5011
+ engineFrameId: Number.isSafeInteger(frameId) && frameId > 0 ? frameId : null,
5012
+ captureReady: document.documentElement.dataset.forgeaxCaptureReady ?? null,
5013
+ singleHtml: singleHtmlState?.witness?.() ?? null,
5014
+ userAgent: navigator.userAgent
5015
+ };
5016
+ }, FORGEAX_FRAME_SUBMITTED_DATASET);
5017
+ }
5018
+ function isSoftwareAdapter(runtime) {
5019
+ const witness = Object.values(runtime.adapter ?? {}).map((value) => String(value).toLowerCase()).join(" ");
5020
+ return ["swiftshader", "llvmpipe", "lavapipe", "software"].some(
5021
+ (token) => witness.includes(token)
5022
+ );
5023
+ }
5024
+ async function writeRunReport(report) {
5025
+ await mkdir(dirname(report.report), { recursive: true });
5026
+ await writeFile(report.report, `${JSON.stringify(report, null, 2)}
5027
+ `, "utf8");
5028
+ }
5029
+ function requestProtocol(url) {
5030
+ try {
5031
+ return new URL(url).protocol;
5032
+ } catch {
5033
+ return void 0;
5034
+ }
5035
+ }
5036
+ function requestRecord(request, target, candidateUrl) {
5037
+ const url = request.url();
5038
+ const protocol = requestProtocol(url);
5039
+ const requestDocument = url.split(/[?#]/, 1)[0];
5040
+ const candidateDocument = candidateUrl.split(/[?#]/, 1)[0];
5041
+ return {
5042
+ url,
5043
+ resourceType: request.resourceType(),
5044
+ status: null,
5045
+ failed: false,
5046
+ failure: null,
5047
+ resourceMiss: target.kind === "single-html" && protocol === "file:" && requestDocument !== candidateDocument
5048
+ };
5049
+ }
5050
+ function summarizeRequests(records) {
5051
+ return {
5052
+ documents: records.filter((record) => record.resourceType === "document").length,
5053
+ http: records.filter((record) => requestProtocol(record.url) === "http:").length,
5054
+ https: records.filter((record) => requestProtocol(record.url) === "https:").length,
5055
+ failed: records.filter((record) => record.failed).length,
5056
+ resourceMisses: records.filter((record) => record.resourceMiss).length,
5057
+ entries: records.map((record) => ({ ...record }))
5058
+ };
5059
+ }
5060
+ function singleHtmlRuntimeClean(target, runtime, requests) {
5061
+ if (target.kind !== "single-html") return true;
5062
+ const singleHtml = runtime?.singleHtml;
5063
+ return singleHtml?.ready === true && singleHtml.resourceMisses.length === 0 && singleHtml.externalRequests.length === 0 && requests.http === 0 && requests.https === 0 && requests.failed === 0 && requests.resourceMisses === 0;
5064
+ }
5065
+ function markResponse(records, response) {
5066
+ const record = records.get(response.request());
5067
+ if (record === void 0) return;
5068
+ record.status = response.status();
5069
+ if (record.status >= 400) record.resourceMiss = true;
5070
+ }
5071
+ function markFailed(records, request) {
5072
+ const record = records.get(request);
5073
+ if (record === void 0) return;
5074
+ record.failed = true;
5075
+ record.failure = request.failure()?.errorText ?? "request failed";
5076
+ record.resourceMiss = true;
5077
+ }
5078
+ async function openBrowserCaptureSession(root, options) {
5079
+ const backend = options.backend ?? (options.software === true ? "software" : "auto");
5080
+ const launchProfile = options.launchProfile ?? "development";
5081
+ const requestedTarget = options.target ?? { kind: "project" };
5082
+ const target = requestedTarget.kind === "single-html" ? { kind: "single-html", path: resolve(requestedTarget.path) } : { kind: "project" };
5083
+ if (target.kind === "single-html") {
5084
+ if (!target.path.toLowerCase().endsWith(".html")) {
5085
+ fail(
5086
+ "browser-capture-target-invalid",
5087
+ "single-html target path to end with .html",
5088
+ "Pass the exact forgeax package --format single-html candidate.",
5089
+ { path: target.path }
5090
+ );
5091
+ }
5092
+ try {
5093
+ const info = await stat(target.path);
5094
+ if (!info.isFile()) throw new Error("target is not a regular file");
5095
+ } catch (cause) {
5096
+ fail(
5097
+ "browser-capture-target-missing",
5098
+ "single-html target path to exist",
5099
+ "Package the game first, then pass its candidate HTML path.",
5100
+ { path: target.path, reason: cause instanceof Error ? cause.message : String(cause) }
5101
+ );
5102
+ }
5103
+ }
5104
+ if (options.software === true && options.backend !== void 0 && options.backend !== "software") {
5105
+ fail(
5106
+ "browser-capture-option-conflict",
5107
+ "software and backend options to describe the same capture lane",
5108
+ "Use either software: true or backend: software; use backend: auto for a portable capture.",
5109
+ { software: options.software, backend: options.backend }
5110
+ );
5111
+ }
5112
+ const facts = await readProjectFacts(root);
5113
+ if (!facts.ok) {
5114
+ throw new SoftwareCaptureError(
5115
+ facts.error.code,
5116
+ facts.error.expected,
5117
+ facts.error.hint,
5118
+ facts.error.detail
5119
+ );
5120
+ }
5121
+ const width = options.width ?? 1280;
5122
+ const height = options.height ?? 720;
5123
+ const browserPath = await resolveBrowserExecutable(options.browser);
5124
+ if (options.browser !== void 0 && browserPath === void 0) {
5125
+ fail(
5126
+ "browser-capture-browser-missing",
5127
+ `a runnable browser at ${options.browser}`,
5128
+ "Install Chrome/Chromium or pass --browser with an executable path.",
5129
+ { browser: options.browser }
5130
+ );
3377
5131
  }
3378
- const version = options.version ?? await packageVersion();
3379
- const parent = dirname(root);
3380
- await mkdir(parent, { recursive: true });
3381
- let staging;
3382
- let download;
5132
+ const id = options.runId ?? newRunId();
5133
+ const outputDirectory = resolve(
5134
+ facts.value.root,
5135
+ options.outputDir ?? `artifacts/playthrough/${id}`
5136
+ );
5137
+ const reportPath = resolve(
5138
+ facts.value.root,
5139
+ options.report ?? resolve(outputDirectory, "run.json")
5140
+ );
5141
+ const port = target.kind === "project" ? options.port === void 0 || options.port === 0 ? await reservePort() : options.port : void 0;
5142
+ let server;
5143
+ let browser;
5144
+ let page;
5145
+ let display;
3383
5146
  try {
3384
- staging = await mkdtemp(resolve(parent, `.${basename(root)}.forgeax-sdk-staging-`));
3385
- download = await mkdtemp(resolve(parent, `.${basename(root)}.forgeax-sdk-download-`));
3386
- await execFileAsync4(
3387
- process.env.FORGEAX_NPM_CLIENT ?? "npm",
3388
- [
3389
- "install",
3390
- "--prefix",
3391
- download,
3392
- "--ignore-scripts",
3393
- "--no-package-lock",
3394
- "--no-save",
3395
- "--fund=false",
3396
- "--audit=false",
3397
- `@forgeax/engine-sdk@${version}`
3398
- ],
3399
- { env: { ...process.env, CI: "true" }, maxBuffer: 16 * 1024 * 1024 }
3400
- );
3401
- const carrier = resolve(download, "node_modules", "@forgeax", "engine-sdk", "sdk");
3402
- const manifest = JSON.parse(await readFile(resolve(carrier, "sdk-manifest.json"), "utf8"));
3403
- if (manifest.sdkVersion !== version) throw new Error("sdk-carrier-version-mismatch");
3404
- for (const name of await readdir(carrier)) {
3405
- await cp(resolve(carrier, name), resolve(staging, name), {
3406
- recursive: true,
3407
- errorOnExist: true,
3408
- force: false
5147
+ if (target.kind === "project") {
5148
+ const { createServer: createServer3 } = await import('vite');
5149
+ if (port === void 0) throw new Error("project capture did not reserve a port");
5150
+ server = await createServer3(
5151
+ await createViteConfig(facts.value, "serve", "/", {
5152
+ server: { port, strictPort: true }
5153
+ })
5154
+ );
5155
+ await server.listen(port);
5156
+ }
5157
+ display = await startVirtualDisplay(width, height);
5158
+ const captureDisplay = display;
5159
+ const icd = await lavapipeIcd();
5160
+ const consoleErrors = [];
5161
+ const pageErrors = [];
5162
+ const requestRecords = [];
5163
+ const requestByObject = /* @__PURE__ */ new Map();
5164
+ const hasDisplay = display.value !== void 0;
5165
+ const headless = options.headless ?? (!hasDisplay && process.platform !== "linux");
5166
+ const baseUrl = target.kind === "project" ? new URL(server?.resolvedUrls?.local[0] ?? `http://127.0.0.1:${port ?? 0}/`) : void 0;
5167
+ const openPage = async (launchBackend) => {
5168
+ const browserEnvironment = {
5169
+ ...Object.fromEntries(
5170
+ Object.entries(process.env).filter(
5171
+ (entry) => entry[1] !== void 0
5172
+ )
5173
+ ),
5174
+ ...launchBackend === "software" ? { LIBGL_ALWAYS_SOFTWARE: "1" } : {},
5175
+ ...captureDisplay.value === void 0 ? {} : { DISPLAY: captureDisplay.value }
5176
+ };
5177
+ const launchOptions = {
5178
+ headless,
5179
+ env: browserEnvironment,
5180
+ args: browserLaunchArgs(launchBackend, launchProfile),
5181
+ ...browserPath === void 0 ? {} : { executablePath: browserPath }
5182
+ };
5183
+ browser = await chromium.launch(launchOptions);
5184
+ page = await browser.newPage({
5185
+ viewport: { width, height },
5186
+ screen: { width, height },
5187
+ deviceScaleFactor: 1,
5188
+ colorScheme: "light",
5189
+ locale: "en-US",
5190
+ timezoneId: "UTC",
5191
+ serviceWorkers: "block"
3409
5192
  });
5193
+ page.on("console", (message) => {
5194
+ if (message.type() === "error") consoleErrors.push(message.text());
5195
+ });
5196
+ page.on("pageerror", (error) => pageErrors.push(String(error)));
5197
+ const captureUrl2 = target.kind === "single-html" ? new URL(pathToFileURL(target.path).href) : new URL(baseUrl?.href ?? "http://127.0.0.1/");
5198
+ page.on("request", (request) => {
5199
+ const record = requestRecord(request, target, captureUrl2.href);
5200
+ requestRecords.push(record);
5201
+ requestByObject.set(request, record);
5202
+ });
5203
+ page.on("response", (response) => markResponse(requestByObject, response));
5204
+ page.on("requestfailed", (request) => markFailed(requestByObject, request));
5205
+ if (options.deterministic === true) captureUrl2.searchParams.set("forgeaxCapture", "1");
5206
+ await page.goto(captureUrl2.href, { waitUntil: "domcontentloaded", timeout: 12e4 });
5207
+ await page.waitForFunction(
5208
+ (requireUi) => {
5209
+ const canvas = document.querySelector("canvas");
5210
+ if (!(canvas instanceof HTMLCanvasElement) || canvas.width <= 0 || canvas.height <= 0)
5211
+ return false;
5212
+ if (!requireUi) return true;
5213
+ const uiRoot = document.querySelector("#game-ui");
5214
+ return uiRoot !== null && uiRoot.childElementCount > 0;
5215
+ },
5216
+ options.requireUi === true,
5217
+ { timeout: 12e4 }
5218
+ );
5219
+ return runtimeWitness(page);
5220
+ };
5221
+ const initialLaunchBackend = backend === "software" ? "software" : "hardware";
5222
+ let runtime = await openPage(initialLaunchBackend);
5223
+ let observed = observedBackend(runtime);
5224
+ if (backend === "auto" && runtime.adapter === null) {
5225
+ await browser?.close();
5226
+ browser = void 0;
5227
+ page = void 0;
5228
+ consoleErrors.length = 0;
5229
+ pageErrors.length = 0;
5230
+ requestRecords.length = 0;
5231
+ requestByObject.clear();
5232
+ runtime = await openPage("software");
5233
+ observed = observedBackend(runtime);
3410
5234
  }
3411
- if (!targetExists) {
3412
- await rename(staging, root);
3413
- staging = void 0;
3414
- } else {
3415
- for (const name of await readdir(staging)) {
3416
- await rename(resolve(staging, name), resolve(root, name));
3417
- }
5235
+ const captureUrl = target.kind === "single-html" ? new URL(pathToFileURL(target.path).href) : new URL(baseUrl?.href ?? "http://127.0.0.1/");
5236
+ if (options.deterministic === true) captureUrl.searchParams.set("forgeaxCapture", "1");
5237
+ if (page === void 0 || browser === void 0) {
5238
+ fail(
5239
+ "browser-capture-browser-unavailable",
5240
+ "the browser capture session to open",
5241
+ "Inspect the browser launch diagnostic and retry with --browser or --headless."
5242
+ );
3418
5243
  }
5244
+ const captures = [];
5245
+ let latestRuntime = runtime;
5246
+ let closed = false;
5247
+ const report = {
5248
+ schemaVersion: "2.0.0",
5249
+ runId: id,
5250
+ ok: false,
5251
+ mode: "browser-compositor",
5252
+ root: facts.value.root,
5253
+ url: captureUrl.href,
5254
+ target,
5255
+ launchProfile,
5256
+ report: reportPath,
5257
+ backendRequested: backend,
5258
+ backend: observed,
5259
+ softwareRequested: backend === "software",
5260
+ deterministicRequested: options.deterministic === true,
5261
+ viewport: {
5262
+ width,
5263
+ height,
5264
+ deviceScaleFactor: 1,
5265
+ colorProfile: "srgb",
5266
+ colorScheme: "light",
5267
+ locale: "en-US",
5268
+ timezone: "UTC"
5269
+ },
5270
+ browser: { version: browser.version(), executable: browserPath ?? "playwright-managed" },
5271
+ display: display.value ?? null,
5272
+ lavapipeIcd: icd ?? null,
5273
+ captures,
5274
+ requests: summarizeRequests(requestRecords),
5275
+ singleHtml: runtime.singleHtml,
5276
+ consoleErrors,
5277
+ pageErrors,
5278
+ boundary: "Browser-compositor capture is visual iteration evidence, not physical-GPU performance, HDR-display output, or release acceptance."
5279
+ };
5280
+ await writeRunReport(report);
5281
+ const updateReport = async () => {
5282
+ const requests = summarizeRequests(requestRecords);
5283
+ Object.assign(report, {
5284
+ requests,
5285
+ singleHtml: latestRuntime?.singleHtml ?? null,
5286
+ ok: captures.length > 0 && captures.every((capture) => capture.ok) && consoleErrors.length === 0 && pageErrors.length === 0 && singleHtmlRuntimeClean(target, latestRuntime, requests)
5287
+ });
5288
+ await writeRunReport(report);
5289
+ };
5290
+ const close = async () => {
5291
+ if (closed) return;
5292
+ closed = true;
5293
+ await Promise.allSettled([browser?.close(), display?.close(), server?.close()]);
5294
+ Object.assign(report, { closedAt: (/* @__PURE__ */ new Date()).toISOString() });
5295
+ await updateReport();
5296
+ };
3419
5297
  return {
3420
- ok: true,
3421
- value: {
3422
- root,
3423
- sdkVersion: version,
3424
- source: "@forgeax/engine-sdk",
3425
- next: { cwd: root, argv: ["node", "./bin/forgeax.mjs", "init"] }
3426
- }
5298
+ page,
5299
+ url: captureUrl.href,
5300
+ reportPath,
5301
+ async capture(checkpoint, captureOptions = {}) {
5302
+ if (closed) {
5303
+ fail(
5304
+ "browser-capture-session-closed",
5305
+ "capture to run inside an open browser session",
5306
+ "Open one session, complete all checkpoints, then close it.",
5307
+ { report: reportPath }
5308
+ );
5309
+ }
5310
+ const activePage = page;
5311
+ if (activePage === void 0) {
5312
+ fail(
5313
+ "browser-capture-browser-unavailable",
5314
+ "an open Playwright page",
5315
+ "Inspect the browser launch diagnostic and retry the capture."
5316
+ );
5317
+ }
5318
+ const expectedReady = checkpoint ?? (options.deterministic === true ? "true" : void 0);
5319
+ await activePage.waitForFunction(
5320
+ ({ datasetKey }) => Number(document.documentElement.dataset[datasetKey]) > 0,
5321
+ { datasetKey: FORGEAX_FRAME_SUBMITTED_DATASET },
5322
+ { timeout: 12e4 }
5323
+ );
5324
+ if (expectedReady !== void 0) {
5325
+ await activePage.waitForFunction(
5326
+ (expected) => document.documentElement.dataset.forgeaxCaptureReady === expected,
5327
+ expectedReady,
5328
+ { timeout: 12e4 }
5329
+ );
5330
+ }
5331
+ await waitForCompositor(activePage);
5332
+ let captured = await screenshotWithWitness(activePage);
5333
+ const deadline = Date.now() + 12e4;
5334
+ while (!captured.pixels.rendered && Date.now() < deadline) {
5335
+ await activePage.waitForTimeout(500);
5336
+ await waitForCompositor(activePage);
5337
+ captured = await screenshotWithWitness(activePage);
5338
+ }
5339
+ const waitMs = captureOptions.waitMs ?? 0;
5340
+ if (waitMs > 0) {
5341
+ await activePage.waitForTimeout(waitMs);
5342
+ await waitForCompositor(activePage);
5343
+ captured = await screenshotWithWitness(activePage);
5344
+ while (!captured.pixels.rendered && Date.now() < deadline) {
5345
+ await activePage.waitForTimeout(500);
5346
+ await waitForCompositor(activePage);
5347
+ captured = await screenshotWithWitness(activePage);
5348
+ }
5349
+ }
5350
+ const runtime2 = await runtimeWitness(activePage);
5351
+ latestRuntime = runtime2;
5352
+ const requestSummary = summarizeRequests(requestRecords);
5353
+ const uiPresent = runtime2.domUi.rootChildren > 0;
5354
+ const requireUi = captureOptions.requireUi ?? options.requireUi ?? false;
5355
+ const captureBackend = observedBackend(runtime2);
5356
+ if (captureBackend !== "unknown") Object.assign(report, { backend: captureBackend });
5357
+ const backendMatches = backend === "auto" || captureBackend === backend;
5358
+ const ok2 = runtime2.canvas !== null && captured.pixels.rendered && backendMatches && runtime2.engineFrameId !== null && consoleErrors.length === 0 && pageErrors.length === 0 && singleHtmlRuntimeClean(target, runtime2, requestSummary) && (expectedReady === void 0 || runtime2.captureReady === expectedReady) && (!requireUi || uiPresent);
5359
+ const index = captures.length + 1;
5360
+ const output = resolve(
5361
+ facts.value.root,
5362
+ captureOptions.output ?? resolve(
5363
+ outputDirectory,
5364
+ `${String(index).padStart(3, "0")}-${checkpointSlug(checkpoint)}.png`
5365
+ )
5366
+ );
5367
+ await mkdir(dirname(output), { recursive: true });
5368
+ await writeFile(output, captured.png);
5369
+ const record = {
5370
+ index,
5371
+ checkpoint: checkpoint ?? null,
5372
+ ok: ok2,
5373
+ output,
5374
+ digest: captureDigest(captured.png),
5375
+ pixels: captured.pixels,
5376
+ runtime: runtime2
5377
+ };
5378
+ captures.push(record);
5379
+ await updateReport();
5380
+ if (!ok2) {
5381
+ fail(
5382
+ `${backend === "software" ? "software" : "browser"}-capture-runtime-failed`,
5383
+ "non-flat canvas pixels, the requested browser backend/checkpoint, and no browser errors",
5384
+ "Inspect the run report and repair the first browser runtime failure.",
5385
+ { checkpoint: checkpoint ?? null, output, report: reportPath }
5386
+ );
5387
+ }
5388
+ return record;
5389
+ },
5390
+ report: () => report,
5391
+ close
3427
5392
  };
3428
5393
  } catch (cause) {
3429
- return { ok: false, error: commandError(cause, "sdk-install-failed") };
3430
- } finally {
3431
- if (staging !== void 0) await rm(staging, { recursive: true, force: true });
3432
- if (download !== void 0) await rm(download, { recursive: true, force: true });
5394
+ await Promise.allSettled([browser?.close(), display?.close(), server?.close()]);
5395
+ throw cause;
3433
5396
  }
3434
5397
  }
3435
- var execFileAsync4;
3436
- var init_sdk_install = __esm({
3437
- "src/sdk-install.ts"() {
3438
- init_project();
3439
- execFileAsync4 = promisify(execFile);
3440
- }
3441
- });
3442
- async function shaderCheckCommand(options = {}) {
3443
- const facts = await readProjectFacts(options.root);
3444
- if (!facts.ok) return facts;
3445
- const target = options.path === void 0 ? void 0 : resolve(facts.value.root, options.path);
3446
- if (target !== void 0) {
3447
- try {
3448
- await readFile(target);
3449
- } catch (cause) {
3450
- return { ok: false, error: commandError(cause, "shader-source-unreadable") };
5398
+ function createBrowserCapture(root) {
5399
+ const sessions = /* @__PURE__ */ new Set();
5400
+ return {
5401
+ async open(options) {
5402
+ const session = await openBrowserCaptureSession(root, options);
5403
+ sessions.add(session);
5404
+ return session;
5405
+ },
5406
+ async close() {
5407
+ await Promise.allSettled([...sessions].map((session) => session.close()));
5408
+ sessions.clear();
3451
5409
  }
5410
+ };
5411
+ }
5412
+ function captureCommandError(cause, legacySoftware = false) {
5413
+ if (cause instanceof SoftwareCaptureError) {
5414
+ const code = legacySoftware && cause.code.startsWith("browser-capture-") ? cause.code.replace(/^browser-capture-/, "software-capture-") : cause.code;
5415
+ return {
5416
+ code,
5417
+ expected: cause.expected,
5418
+ hint: cause.hint,
5419
+ detail: cause.detail
5420
+ };
3452
5421
  }
3453
- const temporary = await mkdtemp(resolve(tmpdir(), "forgeax-shader-check-"));
3454
- const previous = process.cwd();
5422
+ return commandError(cause, legacySoftware ? "software-capture-failed" : "browser-capture-failed");
5423
+ }
5424
+ async function browserCaptureCommand(options) {
5425
+ const root = options.root ?? process.cwd();
5426
+ const output = resolve(root, options.output ?? "artifacts/capture/game-ui.png");
5427
+ const browser = createBrowserCapture(root);
3455
5428
  try {
3456
- process.chdir(facts.value.root);
3457
- const config = await createViteConfig(facts.value, "build");
3458
- await build({
3459
- ...config,
3460
- build: { ...config.build, outDir: temporary, emptyOutDir: true, write: true }
5429
+ const backend = options.backend ?? (options.software === true ? "software" : "auto");
5430
+ const session = await browser.open({
5431
+ backend,
5432
+ ...backend === "software" ? { software: true } : {},
5433
+ ...options.browser === void 0 ? {} : { browser: options.browser },
5434
+ ...options.width === void 0 ? {} : { width: options.width },
5435
+ ...options.height === void 0 ? {} : { height: options.height },
5436
+ ...options.port === void 0 ? {} : { port: options.port },
5437
+ ...options.headless === void 0 ? {} : { headless: options.headless },
5438
+ requireUi: options.requireUi === true,
5439
+ deterministic: options.deterministic === true,
5440
+ outputDir: dirname(output),
5441
+ report: evidencePath(output)
3461
5442
  });
3462
- return { ok: true, value: { root: facts.value.root, path: target ?? null } };
5443
+ await session.capture(options.deterministic === true ? "true" : void 0, {
5444
+ output,
5445
+ waitMs: options.waitMs ?? 4e3,
5446
+ requireUi: options.requireUi === true
5447
+ });
5448
+ await session.close();
5449
+ return { ok: true, value: session.report() };
3463
5450
  } catch (cause) {
3464
- return { ok: false, error: commandError(cause, "shader-check-failed") };
5451
+ return { ok: false, error: captureCommandError(cause) };
3465
5452
  } finally {
3466
- process.chdir(previous);
3467
- await rm(temporary, { recursive: true, force: true });
5453
+ await browser.close();
3468
5454
  }
3469
5455
  }
3470
- var init_shader_check = __esm({
3471
- "src/shader-check.ts"() {
5456
+ async function softwareCaptureCommand(options) {
5457
+ const result = await browserCaptureCommand({ ...options, backend: "software", software: true });
5458
+ if (result.ok) return result;
5459
+ return { ok: false, error: captureCommandErrorFromResult(result.error) };
5460
+ }
5461
+ function captureCommandErrorFromResult(error, legacySoftware) {
5462
+ if (!error.code.startsWith("browser-capture-")) return error;
5463
+ return { ...error, code: error.code.replace(/^browser-capture-/, "software-capture-") };
5464
+ }
5465
+ var SoftwareCaptureError;
5466
+ var init_software_capture = __esm({
5467
+ "src/software-capture.ts"() {
3472
5468
  init_host();
3473
5469
  init_project();
5470
+ SoftwareCaptureError = class extends Error {
5471
+ constructor(code, expected, hint, detail) {
5472
+ super(`${code}: ${hint}`);
5473
+ this.code = code;
5474
+ this.expected = expected;
5475
+ this.hint = hint;
5476
+ this.detail = detail;
5477
+ this.name = "SoftwareCaptureError";
5478
+ }
5479
+ code;
5480
+ expected;
5481
+ hint;
5482
+ detail;
5483
+ };
3474
5484
  }
3475
5485
  });
3476
5486
 
@@ -3482,12 +5492,17 @@ __export(commands_exports, {
3482
5492
  assetInspectCommand: () => assetInspectCommand,
3483
5493
  assetListCommand: () => assetListCommand,
3484
5494
  assetVerifyCommand: () => assetVerifyCommand,
5495
+ browserCaptureCommand: () => browserCaptureCommand,
3485
5496
  buildCommand: () => buildCommand,
3486
5497
  createCliRhiDebugOperationContext: () => createCliRhiDebugOperationContext,
3487
5498
  createRhiDebugOperationContext: () => createRhiDebugOperationContext,
3488
5499
  devCommand: () => devCommand,
3489
5500
  discoverRhiDebugOperations: () => discoverRhiDebugOperations,
3490
5501
  doctorCommand: () => doctorCommand,
5502
+ engineDoctorCommand: () => engineDoctorCommand,
5503
+ engineStatusCommand: () => engineStatusCommand,
5504
+ engineUnlinkCommand: () => engineUnlinkCommand,
5505
+ engineUseLocalCommand: () => engineUseLocalCommand,
3491
5506
  initCommand: () => initCommand,
3492
5507
  newCommand: () => newCommand,
3493
5508
  packageCommand: () => packageCommand,
@@ -3502,6 +5517,7 @@ __export(commands_exports, {
3502
5517
  shaderCheckCommand: () => shaderCheckCommand,
3503
5518
  skillInstallCommand: () => skillInstallCommand,
3504
5519
  skillVerifyCommand: () => skillVerifyCommand,
5520
+ softwareCaptureCommand: () => softwareCaptureCommand,
3505
5521
  testCommand: () => testCommand
3506
5522
  });
3507
5523
  function runRhiDebugCommand(name, input, context) {
@@ -3545,31 +5561,47 @@ function releaseSlug(name) {
3545
5561
  async function packageCommand(options = {}) {
3546
5562
  const facts = await readProjectFacts(options.root);
3547
5563
  if (!facts.ok) return facts;
5564
+ const format = options.format ?? "web-zip";
5565
+ if (format !== "web-zip" && format !== "single-html") return packageFormatError(String(format));
5566
+ const defaultOutput = format === "single-html" ? `release/${releaseSlug(facts.value.name)}-offline.html` : `release/${releaseSlug(facts.value.name)}-web.zip`;
5567
+ const output = resolve(facts.value.root, options.output ?? defaultOutput);
5568
+ const expectedSuffix = format === "single-html" ? ".html" : ".zip";
5569
+ if (!output.toLowerCase().endsWith(expectedSuffix)) {
5570
+ return packageOutputError(output, format);
5571
+ }
5572
+ const distRoot = resolve(facts.value.root, "dist");
5573
+ const outputRelativeToDist = relative(distRoot, output);
5574
+ if (outputRelativeToDist === "" || !outputRelativeToDist.startsWith("..") && outputRelativeToDist !== "..") {
5575
+ return {
5576
+ ok: false,
5577
+ error: {
5578
+ code: "release-output-inside-dist",
5579
+ expected: "the release artifact to live outside the derived dist directory",
5580
+ hint: "Use --output release/<game>-web.zip or release/<game>-offline.html.",
5581
+ detail: { output, distRoot, format }
5582
+ }
5583
+ };
5584
+ }
3548
5585
  const built = await buildCommand({
3549
5586
  root: facts.value.root,
3550
5587
  base: "./",
3551
5588
  ...options.json === void 0 ? {} : { json: options.json }
3552
5589
  });
3553
5590
  if (!built.ok) return built;
3554
- const distRoot = resolve(facts.value.root, "dist");
3555
5591
  const verified = await verifyDist(distRoot);
3556
5592
  if (!verified.ok) return verified;
3557
- const archive = resolve(
3558
- facts.value.root,
3559
- options.output ?? `release/${releaseSlug(facts.value.name)}-web.zip`
3560
- );
3561
- const archiveRelativeToDist = relative(distRoot, archive);
3562
- if (archiveRelativeToDist === "" || !archiveRelativeToDist.startsWith("..")) {
3563
- return {
3564
- ok: false,
3565
- error: {
3566
- code: "release-output-inside-dist",
3567
- expected: "the release archive to live outside the derived dist directory",
3568
- hint: "Use --output release/<game>-web.zip or another path outside dist.",
3569
- detail: { archive, distRoot }
3570
- }
3571
- };
5593
+ if (format === "single-html") {
5594
+ const indexHtml = await readFile(resolve(distRoot, "index.html"), "utf8");
5595
+ const bundle = await bundleSingleHtmlEntry(distRoot, indexHtml, facts.value.root);
5596
+ if (!bundle.ok) return bundle;
5597
+ return writeSingleHtml({
5598
+ distRoot,
5599
+ output,
5600
+ manifest: verified.value,
5601
+ bundle: bundle.value
5602
+ });
3572
5603
  }
5604
+ const archive = output;
3573
5605
  const temporaryArchive = `${archive}.partial-${process.pid}`;
3574
5606
  const checksumPath = `${archive}.sha256`;
3575
5607
  const temporaryChecksum = `${checksumPath}.partial-${process.pid}`;
@@ -3582,11 +5614,11 @@ async function packageCommand(options = {}) {
3582
5614
  paths.map(async (path) => ({ path, bytes: await readFile(resolve(distRoot, path)) }))
3583
5615
  );
3584
5616
  const bytes = createZip(entries2);
3585
- const sha256 = createHash("sha256").update(bytes).digest("hex");
5617
+ const sha2562 = createHash("sha256").update(bytes).digest("hex");
3586
5618
  const manifestBytes = await readFile(resolve(distRoot, "forgeax-dist.json"));
3587
5619
  await mkdir(dirname(archive), { recursive: true });
3588
5620
  await writeFile(temporaryArchive, bytes);
3589
- await writeFile(temporaryChecksum, `${sha256} ${basename(archive)}
5621
+ await writeFile(temporaryChecksum, `${sha2562} ${basename(archive)}
3590
5622
  `);
3591
5623
  await rename(temporaryChecksum, checksumPath);
3592
5624
  await rename(temporaryArchive, archive);
@@ -3602,7 +5634,7 @@ async function packageCommand(options = {}) {
3602
5634
  delivery: "bundled-runtime",
3603
5635
  wasm: verified.value.artifacts.some((artifact) => artifact.path.endsWith(".wasm"))
3604
5636
  },
3605
- archive: { path: archive, bytes: bytes.byteLength, sha256 },
5637
+ archive: { path: archive, bytes: bytes.byteLength, sha256: sha2562 },
3606
5638
  checksumPath,
3607
5639
  distManifestSha256: createHash("sha256").update(manifestBytes).digest("hex"),
3608
5640
  run: {
@@ -3626,12 +5658,12 @@ async function devCommand(options = {}) {
3626
5658
  const previous = process.cwd();
3627
5659
  let server;
3628
5660
  try {
3629
- const { createServer: createServer2 } = await import('vite');
5661
+ const { createServer: createServer3 } = await import('vite');
3630
5662
  const { createViteConfig: createViteConfig2 } = await Promise.resolve().then(() => (init_host(), host_exports));
3631
5663
  const port = resolveProjectPort(options.port);
3632
5664
  const vitePort = { ...port, port: await materializeViteDevPort(port.port) };
3633
5665
  process.chdir(facts.value.root);
3634
- server = await createServer2(
5666
+ server = await createServer3(
3635
5667
  await createViteConfig2(facts.value, "serve", "/", {
3636
5668
  server: vitePort
3637
5669
  })
@@ -3739,18 +5771,34 @@ var init_commands = __esm({
3739
5771
  init_dist();
3740
5772
  init_host();
3741
5773
  init_project();
5774
+ init_single_html();
3742
5775
  init_types();
3743
5776
  init_assets();
3744
5777
  init_bootstrap_commands();
5778
+ init_engine_binding();
3745
5779
  init_plugin_authoring();
3746
5780
  init_cli_context();
3747
5781
  init_operations();
3748
5782
  init_sdk_install();
3749
5783
  init_shader_check();
3750
5784
  init_skill_install();
5785
+ init_software_capture();
3751
5786
  init_operations();
3752
5787
  }
3753
5788
  });
5789
+ function createNativePreviewCatalog(host, selectedName) {
5790
+ return new Map(
5791
+ nativePreviewPlugins.filter(([name]) => selectedName === void 0 || name === selectedName).map(([name, plugin]) => [
5792
+ name,
5793
+ {
5794
+ realm: "host",
5795
+ load: async () => ({
5796
+ default: bindPreviewHost(plugin, host)
5797
+ })
5798
+ }
5799
+ ])
5800
+ );
5801
+ }
3754
5802
  var nativePreviewPlugins, nativePreviewTools, nativePreviewDescriptors;
3755
5803
  var init_preview_catalog = __esm({
3756
5804
  "src/tools/preview-catalog.ts"() {
@@ -4225,11 +6273,11 @@ async function runProjectTool(binding, args2, options) {
4225
6273
  );
4226
6274
  await loader.root.update(entries2);
4227
6275
  await loader.await();
4228
- const { createToolRuntime: createToolRuntime4 } = await import('@forgeax/engine-tool-runtime');
4229
- const { createContextCapabilityResolver } = await import('@forgeax/engine-plugin');
4230
- return await createToolRuntime4([contribution]).run(contribution, args2, {
6276
+ const { createToolRuntime: createToolRuntime5 } = await import('@forgeax/engine-tool-runtime');
6277
+ const { createContextCapabilityResolver: createContextCapabilityResolver2 } = await import('@forgeax/engine-plugin');
6278
+ return await createToolRuntime5([contribution]).run(contribution, args2, {
4231
6279
  ...options,
4232
- capabilityResolver: createContextCapabilityResolver(ctx)
6280
+ capabilityResolver: createContextCapabilityResolver2(ctx)
4233
6281
  }).terminal;
4234
6282
  } catch (cause) {
4235
6283
  return activationFailure(cause);
@@ -4241,6 +6289,50 @@ var init_project_tools = __esm({
4241
6289
  "src/tools/project-tools.ts"() {
4242
6290
  }
4243
6291
  });
6292
+
6293
+ // src/tools/contributions.ts
6294
+ var contributions_exports = {};
6295
+ __export(contributions_exports, {
6296
+ createAuthorContribution: () => createAuthorContribution,
6297
+ createBuildContribution: () => createBuildContribution,
6298
+ createDefaultContributions: () => createDefaultContributions
6299
+ });
6300
+ function commandFailure2(error) {
6301
+ return { ok: false, error: { ...error, detail: error.detail } };
6302
+ }
6303
+ function createBuildContribution(projectRoot2 = process.cwd()) {
6304
+ return defineTool(
6305
+ projectBuildDescriptor,
6306
+ async (options) => {
6307
+ const { buildCommand: buildCommand2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
6308
+ const result = await buildCommand2({ ...options, root: options.root ?? projectRoot2 });
6309
+ return result.ok ? result.value : commandFailure2(result.error);
6310
+ }
6311
+ );
6312
+ }
6313
+ function createAuthorContribution(projectRoot2 = process.cwd()) {
6314
+ return defineTool(
6315
+ authorPluginInstallDescriptor,
6316
+ async (options) => {
6317
+ const { pluginInstallCommand: pluginInstallCommand2 } = await Promise.resolve().then(() => (init_plugin_authoring(), plugin_authoring_exports));
6318
+ const result = await pluginInstallCommand2({ ...options, root: options.root ?? projectRoot2 });
6319
+ return result.ok ? result.value : commandFailure2(result.error);
6320
+ }
6321
+ );
6322
+ }
6323
+ function createDefaultContributions(projectRoot2 = process.cwd()) {
6324
+ return [
6325
+ createBuildContribution(projectRoot2),
6326
+ createAuthorContribution(projectRoot2),
6327
+ ...nativePreviewTools
6328
+ ];
6329
+ }
6330
+ var init_contributions = __esm({
6331
+ "src/tools/contributions.ts"() {
6332
+ init_catalog();
6333
+ init_preview_catalog();
6334
+ }
6335
+ });
4244
6336
  function analyzePreviewArtifacts(request) {
4245
6337
  const required = request.required ?? [];
4246
6338
  const validated = request.manifest.schemaVersion === "1.0.0" ? required.some((kind) => kind !== "rhi-tape" && kind !== "png" && kind !== "profile-capture") ? {
@@ -4251,7 +6343,7 @@ function analyzePreviewArtifacts(request) {
4251
6343
  hint: "Request one of rhi-tape, png, or profile-capture for a v1 manifest.",
4252
6344
  detail: { reason: "unsupported required evidence kind" }
4253
6345
  }
4254
- } : validateArtifactManifest(request.manifest, required) : validatePreviewArtifactManifest(
6346
+ } : validateArtifactManifest(request.manifest, required) : validatePreviewArtifactManifest$1(
4255
6347
  request.manifest,
4256
6348
  (request.required ?? []).filter(
4257
6349
  (role) => role === "report" || role === "rhi-tape" || role === "capture" || role === "fresh-replay" || role === "profile-capture" || role === "contact-sheet"
@@ -4277,77 +6369,25 @@ function createPreviewContribution() {
4277
6369
  error: retiredPreviewOperationFailure
4278
6370
  }));
4279
6371
  }
4280
- function createOfflineAnalysisContribution() {
4281
- return defineTool(
4282
- {
4283
- ...previewOfflineAnalysisDescriptor
4284
- },
4285
- (request) => analyzePreviewArtifacts(request)
4286
- );
4287
- }
4288
- function createPreviewContributions() {
4289
- return [
4290
- createPreviewContribution(),
4291
- createOfflineAnalysisContribution()
4292
- ];
4293
- }
4294
- function createDomainPreviewContributions() {
4295
- return [
4296
- createMaterialPreviewContribution(),
4297
- createMeshPreviewContribution(),
4298
- createVfxPreviewContribution(),
4299
- createTexturePreviewContribution()
4300
- ];
4301
- }
4302
- var init_preview_contributions = __esm({
4303
- "src/tools/preview-contributions.ts"() {
4304
- init_catalog();
4305
- init_offline_analysis();
4306
- init_preview_migration();
4307
- }
4308
- });
4309
-
4310
- // src/tools/contributions.ts
4311
- var contributions_exports = {};
4312
- __export(contributions_exports, {
4313
- createAuthorContribution: () => createAuthorContribution,
4314
- createBuildContribution: () => createBuildContribution,
4315
- createDefaultContributions: () => createDefaultContributions
4316
- });
4317
- function commandFailure2(error) {
4318
- return { ok: false, error: { ...error, detail: error.detail } };
4319
- }
4320
- function createBuildContribution(projectRoot2 = process.cwd()) {
4321
- return defineTool(
4322
- projectBuildDescriptor,
4323
- async (options) => {
4324
- const { buildCommand: buildCommand2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
4325
- const result = await buildCommand2({ ...options, root: options.root ?? projectRoot2 });
4326
- return result.ok ? result.value : commandFailure2(result.error);
4327
- }
4328
- );
4329
- }
4330
- function createAuthorContribution(projectRoot2 = process.cwd()) {
6372
+ function createOfflineAnalysisContribution() {
4331
6373
  return defineTool(
4332
- authorPluginInstallDescriptor,
4333
- async (options) => {
4334
- const { pluginInstallCommand: pluginInstallCommand2 } = await Promise.resolve().then(() => (init_plugin_authoring(), plugin_authoring_exports));
4335
- const result = await pluginInstallCommand2({ ...options, root: options.root ?? projectRoot2 });
4336
- return result.ok ? result.value : commandFailure2(result.error);
4337
- }
6374
+ {
6375
+ ...previewOfflineAnalysisDescriptor
6376
+ },
6377
+ (request) => analyzePreviewArtifacts(request)
4338
6378
  );
4339
6379
  }
4340
- function createDefaultContributions(projectRoot2 = process.cwd()) {
6380
+ function createPreviewContributions() {
4341
6381
  return [
4342
- createBuildContribution(projectRoot2),
4343
- createAuthorContribution(projectRoot2),
4344
- ...createDomainPreviewContributions()
6382
+ createPreviewContribution(),
6383
+ createOfflineAnalysisContribution()
4345
6384
  ];
4346
6385
  }
4347
- var init_contributions = __esm({
4348
- "src/tools/contributions.ts"() {
6386
+ var init_preview_contributions = __esm({
6387
+ "src/tools/preview-contributions.ts"() {
4349
6388
  init_catalog();
4350
- init_preview_contributions();
6389
+ init_offline_analysis();
6390
+ init_preview_migration();
4351
6391
  }
4352
6392
  });
4353
6393
 
@@ -4368,6 +6408,679 @@ var init_runtime = __esm({
4368
6408
  init_preview_contributions();
4369
6409
  }
4370
6410
  });
6411
+ function transportCause(cause) {
6412
+ if (cause instanceof Error) {
6413
+ return {
6414
+ name: cause.name,
6415
+ message: cause.message,
6416
+ ...cause.stack === void 0 ? {} : { stack: cause.stack }
6417
+ };
6418
+ }
6419
+ if (cause === void 0) return "undefined";
6420
+ try {
6421
+ return JSON.parse(JSON.stringify(cause));
6422
+ } catch {
6423
+ return String(cause);
6424
+ }
6425
+ }
6426
+ function dataUriBytes(uri) {
6427
+ const separator = uri.indexOf(",");
6428
+ if (separator < 0) throw new TypeError("preview artifact must be a data URI before publication");
6429
+ return Buffer.from(uri.slice(separator + 1), "base64");
6430
+ }
6431
+ function projectUri(projectRoot2, path) {
6432
+ return relative(projectRoot2, path).split(sep).join("/");
6433
+ }
6434
+ async function allocateLoopbackPort() {
6435
+ const probe = createServer();
6436
+ try {
6437
+ await new Promise((resolve20, reject) => {
6438
+ const onError = (error) => {
6439
+ probe.off("listening", onListening);
6440
+ reject(error);
6441
+ };
6442
+ const onListening = () => {
6443
+ probe.off("error", onError);
6444
+ resolve20();
6445
+ };
6446
+ probe.once("error", onError);
6447
+ probe.once("listening", onListening);
6448
+ probe.listen(0, "127.0.0.1");
6449
+ });
6450
+ const address = probe.address();
6451
+ if (address === null || typeof address === "string") {
6452
+ throw new Error("loopback port probe did not expose a TCP address");
6453
+ }
6454
+ return address.port;
6455
+ } finally {
6456
+ if (probe.listening) {
6457
+ await new Promise((resolve20) => probe.close(() => resolve20()));
6458
+ }
6459
+ }
6460
+ }
6461
+ function sha256(bytes) {
6462
+ return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
6463
+ }
6464
+ function artifactRef(artifact) {
6465
+ const kind = artifact.kind === "report" || artifact.kind === "contact-sheet" ? "tool-result" : artifact.kind;
6466
+ return {
6467
+ kind,
6468
+ digest: artifact.digest,
6469
+ uri: artifact.uri,
6470
+ mediaType: artifact.mediaType,
6471
+ sizeBytes: artifact.byteLength
6472
+ };
6473
+ }
6474
+ async function publishPreviewArtifacts(projectRoot2, runId, result, reportInput) {
6475
+ if (result.manifest.identity.runId !== runId) {
6476
+ throw new Error(
6477
+ `preview artifact run identity mismatch: manifest=${result.manifest.identity.runId} requested=${runId}`
6478
+ );
6479
+ }
6480
+ const runsRoot = join(projectRoot2, ".forgeax", "tool-runs");
6481
+ await mkdir(runsRoot, { recursive: true });
6482
+ const directoryName = runId.replace(/[^a-zA-Z0-9._-]/g, "_");
6483
+ const published = join(runsRoot, directoryName);
6484
+ const finalPaths = {
6485
+ tapeJson: join(published, "rhi-tape.json"),
6486
+ tapeBlob: join(published, "rhi-tape.bin"),
6487
+ capturePng: join(published, "capture.png"),
6488
+ freshReplayPng: join(published, "fresh-replay.png"),
6489
+ profile: join(published, "profile.json"),
6490
+ manifest: join(published, "manifest.json"),
6491
+ report: join(published, "report.json")
6492
+ };
6493
+ const tapeJson = dataUriBytes(result.tape.jsonUri);
6494
+ const tapeBlob = dataUriBytes(result.tape.blobUri);
6495
+ const capturePng = dataUriBytes(result.capturePng.uri);
6496
+ const freshReplayPng = dataUriBytes(result.png.uri);
6497
+ const profile = dataUriBytes(result.profile.uri);
6498
+ const bytesByRole = {
6499
+ "rhi-tape": tapeJson,
6500
+ capture: capturePng,
6501
+ "fresh-replay": freshReplayPng,
6502
+ "profile-capture": profile
6503
+ };
6504
+ const uriByRole = {
6505
+ "rhi-tape": projectUri(projectRoot2, finalPaths.tapeJson),
6506
+ capture: projectUri(projectRoot2, finalPaths.capturePng),
6507
+ "fresh-replay": projectUri(projectRoot2, finalPaths.freshReplayPng),
6508
+ "profile-capture": projectUri(projectRoot2, finalPaths.profile)
6509
+ };
6510
+ const mediaTypeByRole = {
6511
+ "rhi-tape": "application/vnd.forgeax.rhi-tape+json",
6512
+ capture: "image/png",
6513
+ "fresh-replay": "image/png",
6514
+ "profile-capture": "application/vnd.forgeax.profile+json"
6515
+ };
6516
+ const requiredRoles = ["rhi-tape", "capture", "fresh-replay", "profile-capture"];
6517
+ const nonReportArtifacts = result.manifest.artifacts.filter((artifact) => artifact.role !== "report").map((artifact) => {
6518
+ if (!(artifact.role in bytesByRole)) {
6519
+ return artifact;
6520
+ }
6521
+ const role = artifact.role;
6522
+ const bytes = bytesByRole[role];
6523
+ const digest = sha256(bytes);
6524
+ if (artifact.digest !== digest) {
6525
+ throw new Error(`preview artifact digest mismatch for ${artifact.role}`);
6526
+ }
6527
+ return {
6528
+ ...artifact,
6529
+ uri: uriByRole[role],
6530
+ digest,
6531
+ byteLength: bytes.byteLength,
6532
+ mediaType: mediaTypeByRole[role]
6533
+ };
6534
+ });
6535
+ if (requiredRoles.some((role) => !nonReportArtifacts.some((artifact) => artifact.role === role))) {
6536
+ throw new Error(
6537
+ "preview publication is missing capture, fresh-replay, tape, or profile artifact"
6538
+ );
6539
+ }
6540
+ let report;
6541
+ let reportBytes;
6542
+ if (reportInput !== void 0) {
6543
+ if (reportInput.snapshot.digest !== result.manifest.identity.snapshotDigest) {
6544
+ throw new Error("preview report snapshot identity does not match the artifact manifest");
6545
+ }
6546
+ report = createResourcePreviewReport({
6547
+ runId,
6548
+ snapshot: reportInput.snapshot,
6549
+ subject: reportInput.subject,
6550
+ presentation: reportInput.presentation,
6551
+ oracle: reportInput.oracle,
6552
+ artifacts: nonReportArtifacts
6553
+ });
6554
+ reportBytes = new TextEncoder().encode(JSON.stringify(report));
6555
+ }
6556
+ const reportArtifact = reportBytes === void 0 ? void 0 : {
6557
+ owner: "resource-preview",
6558
+ kind: "report",
6559
+ role: "report",
6560
+ uri: projectUri(projectRoot2, finalPaths.report),
6561
+ digest: sha256(reportBytes),
6562
+ byteLength: reportBytes.byteLength,
6563
+ mediaType: "application/vnd.forgeax.resource-preview+json",
6564
+ derivedFrom: nonReportArtifacts.map((artifact) => artifact.digest)
6565
+ };
6566
+ const manifest = createPreviewArtifactManifest({
6567
+ ...result.manifest,
6568
+ artifacts: reportArtifact === void 0 ? nonReportArtifacts : [reportArtifact, ...nonReportArtifacts]
6569
+ });
6570
+ const validated = validatePreviewArtifactManifest(
6571
+ manifest,
6572
+ reportArtifact === void 0 ? requiredRoles : ["report", ...requiredRoles]
6573
+ );
6574
+ if (!validated.ok) throw new Error(JSON.stringify(validated.error.detail));
6575
+ const staging = await mkdtemp(join(runsRoot, ".staging-"));
6576
+ const paths = {
6577
+ tapeJson: join(staging, "rhi-tape.json"),
6578
+ tapeBlob: join(staging, "rhi-tape.bin"),
6579
+ capturePng: join(staging, "capture.png"),
6580
+ freshReplayPng: join(staging, "fresh-replay.png"),
6581
+ profile: join(staging, "profile.json"),
6582
+ manifest: join(staging, "manifest.json"),
6583
+ report: join(staging, "report.json")
6584
+ };
6585
+ try {
6586
+ await Promise.all([
6587
+ writeFile(paths.tapeJson, tapeJson),
6588
+ writeFile(paths.tapeBlob, tapeBlob),
6589
+ writeFile(paths.capturePng, capturePng),
6590
+ writeFile(paths.freshReplayPng, freshReplayPng),
6591
+ writeFile(paths.profile, profile),
6592
+ ...reportBytes === void 0 ? [] : [writeFile(paths.report, reportBytes)]
6593
+ ]);
6594
+ await writeFile(paths.manifest, `${JSON.stringify(manifest, null, 2)}
6595
+ `);
6596
+ await rename(staging, published);
6597
+ } catch (cause) {
6598
+ await rm(staging, { recursive: true, force: true });
6599
+ throw cause;
6600
+ }
6601
+ return {
6602
+ ...result,
6603
+ tape: {
6604
+ ...result.tape,
6605
+ jsonUri: projectUri(projectRoot2, finalPaths.tapeJson),
6606
+ blobUri: projectUri(projectRoot2, finalPaths.tapeBlob)
6607
+ },
6608
+ profile: { ...result.profile, uri: projectUri(projectRoot2, finalPaths.profile) },
6609
+ capturePng: { ...result.capturePng, uri: projectUri(projectRoot2, finalPaths.capturePng) },
6610
+ png: { ...result.png, uri: projectUri(projectRoot2, finalPaths.freshReplayPng) },
6611
+ manifest,
6612
+ artifacts: manifest.artifacts.map(artifactRef)
6613
+ };
6614
+ }
6615
+ function browserFailure(phase, cause, pageErrors = []) {
6616
+ return {
6617
+ ok: false,
6618
+ error: {
6619
+ code: "tool-preview-browser-host-failed",
6620
+ expected: "a real Chromium page, project bootstrap, WebGPU device, and bounded preview run",
6621
+ hint: "Inspect the Browser Host phase and repair the project or local WebGPU capability.",
6622
+ detail: {
6623
+ phase,
6624
+ cause: transportCause(cause),
6625
+ pageErrors
6626
+ }
6627
+ }
6628
+ };
6629
+ }
6630
+ async function runBrowserPreviewHost(projectRoot2, recipe, snapshot, runId, signal, bootstrapRoot = "project-bootstrap", resource, options = {}) {
6631
+ const facts = await readProjectFacts(projectRoot2);
6632
+ if (!facts.ok)
6633
+ return { ok: false, error: { ...facts.error, detail: facts.error.detail } };
6634
+ const config = await createViteConfig(facts.value, "serve", "/", { bootstrapRoot });
6635
+ const cacheDir = await mkdtemp(join(facts.value.root, ".forgeax", ".browser-host-vite-"));
6636
+ let server;
6637
+ try {
6638
+ const port = await allocateLoopbackPort();
6639
+ server = await createServer$1({
6640
+ ...config,
6641
+ cacheDir,
6642
+ logLevel: "silent",
6643
+ optimizeDeps: {
6644
+ ...config.optimizeDeps,
6645
+ // Tool runs must observe the current workspace build, even when a
6646
+ // persistent runner retains Vite's optimized-dependency cache.
6647
+ force: true
6648
+ },
6649
+ server: {
6650
+ ...config.server,
6651
+ host: "127.0.0.1",
6652
+ port,
6653
+ // Vite's port 0 means its default 5173. Bind the probed port exactly so
6654
+ // another project or runner cannot be mistaken for this Browser Host.
6655
+ strictPort: true
6656
+ }
6657
+ });
6658
+ } catch (cause) {
6659
+ await rm(cacheDir, { recursive: true, force: true });
6660
+ throw cause;
6661
+ }
6662
+ const hostStartedAtMs = performance.now();
6663
+ let phase = "server-listen";
6664
+ let browser;
6665
+ let page;
6666
+ let pageErrors = [];
6667
+ let responseDiagnostics = [];
6668
+ const launchBrowser = (headless) => chromium.launch({
6669
+ channel: process.env.FORGEAX_CHROME_CHANNEL ?? "chrome",
6670
+ headless,
6671
+ args: [
6672
+ "--enable-unsafe-webgpu",
6673
+ "--enable-features=Vulkan,UseSkiaRenderer,SharedArrayBuffer",
6674
+ "--use-vulkan=swiftshader",
6675
+ "--use-angle=swiftshader",
6676
+ "--disable-vulkan-surface",
6677
+ "--ignore-gpu-blocklist",
6678
+ "--disable-gpu-driver-bug-workarounds",
6679
+ "--disable-dawn-features=disallow_unsafe_apis",
6680
+ "--autoplay-policy=no-user-gesture-required"
6681
+ ]
6682
+ });
6683
+ const observePage = (target) => {
6684
+ target.on("pageerror", (error) => pageErrors.push(error.message));
6685
+ target.on("console", (message) => {
6686
+ if (message.type() === "error" || message.type() === "warning") {
6687
+ pageErrors.push(`${message.type()}: ${message.text()}`);
6688
+ }
6689
+ });
6690
+ target.on("response", (response) => {
6691
+ if (response.status() >= 400) {
6692
+ responseDiagnostics.push(
6693
+ response.text().then((body) => {
6694
+ pageErrors.push(`HTTP ${response.status()}: ${response.url()} ${body}`);
6695
+ }).catch(() => {
6696
+ pageErrors.push(`HTTP ${response.status()}: ${response.url()}`);
6697
+ })
6698
+ );
6699
+ }
6700
+ });
6701
+ };
6702
+ const closeBrowserProcess = async () => {
6703
+ await page?.close().catch(() => void 0);
6704
+ page = void 0;
6705
+ await browser?.close().catch(() => void 0);
6706
+ browser = void 0;
6707
+ };
6708
+ const abort = () => {
6709
+ void closeBrowserProcess();
6710
+ void server.close();
6711
+ };
6712
+ signal.addEventListener("abort", abort, { once: true });
6713
+ try {
6714
+ if (signal.aborted) throw new Error("Browser Host aborted before launch");
6715
+ await server.listen();
6716
+ const address = server.httpServer?.address();
6717
+ if (address === null || address === void 0 || typeof address === "string") {
6718
+ throw new Error("Vite Browser Host did not expose a loopback TCP address");
6719
+ }
6720
+ phase = "server-transform";
6721
+ const entryTransform = await server.transformRequest("/main.ts");
6722
+ if (entryTransform === null) {
6723
+ throw new Error("Vite Browser Host entry transform returned no module");
6724
+ }
6725
+ phase = "capture-browser-launch";
6726
+ browser = await launchBrowser(recipe.presentation === "hidden");
6727
+ phase = "capture-page-bootstrap";
6728
+ page = await browser.newPage({
6729
+ viewport: recipe.viewport,
6730
+ deviceScaleFactor: 1
6731
+ });
6732
+ observePage(page);
6733
+ const captureUrl = new URL(`http://127.0.0.1:${address.port}/`);
6734
+ captureUrl.searchParams.set("forgeax-tool-recipe", JSON.stringify(recipe));
6735
+ captureUrl.searchParams.set("forgeax-tool-snapshot", JSON.stringify(snapshot));
6736
+ captureUrl.searchParams.set("forgeax-tool-run-id", runId);
6737
+ if (resource !== void 0)
6738
+ captureUrl.searchParams.set("forgeax-resource-preview", JSON.stringify(resource));
6739
+ await page.goto(captureUrl.href, { waitUntil: "networkidle", timeout: 45e3 });
6740
+ await page.waitForFunction(
6741
+ () => globalThis.__forgeaxToolHost?.ready === true,
6742
+ void 0,
6743
+ { timeout: 45e3 }
6744
+ );
6745
+ phase = "capture-run";
6746
+ const captured = await page.evaluate(async () => {
6747
+ const host = globalThis.__forgeaxToolHost;
6748
+ const value2 = await host.capture();
6749
+ return JSON.parse(
6750
+ JSON.stringify(
6751
+ value2,
6752
+ (_key, nested2) => nested2 instanceof Error ? {
6753
+ ...nested2,
6754
+ name: nested2.name,
6755
+ message: nested2.message,
6756
+ stack: nested2.stack
6757
+ } : nested2
6758
+ )
6759
+ );
6760
+ });
6761
+ await Promise.all(responseDiagnostics);
6762
+ if (!captured.ok) return browserFailure("capture-run", captured.error, pageErrors);
6763
+ if (pageErrors.length > 0)
6764
+ return browserFailure("capture-page-runtime", pageErrors[0], pageErrors);
6765
+ const capturePng = await page.screenshot({ type: "png" });
6766
+ const capturedResult = {
6767
+ ...captured.result,
6768
+ capturePng: {
6769
+ uri: `data:image/png;base64,${capturePng.toString("base64")}`,
6770
+ width: recipe.viewport.width,
6771
+ height: recipe.viewport.height
6772
+ }
6773
+ };
6774
+ await closeBrowserProcess();
6775
+ pageErrors = [];
6776
+ responseDiagnostics = [];
6777
+ phase = "replay-browser-launch";
6778
+ browser = await launchBrowser(true);
6779
+ phase = "replay-page-bootstrap";
6780
+ page = await browser.newPage({
6781
+ viewport: recipe.viewport,
6782
+ deviceScaleFactor: 1
6783
+ });
6784
+ observePage(page);
6785
+ const replayUrl = new URL(`http://127.0.0.1:${address.port}/`);
6786
+ replayUrl.searchParams.set("forgeax-tool-replay", "1");
6787
+ await page.goto(replayUrl.href, { waitUntil: "networkidle", timeout: 45e3 });
6788
+ await page.waitForFunction(
6789
+ () => globalThis.__forgeaxToolReplayHost?.ready === true,
6790
+ void 0,
6791
+ { timeout: 45e3 }
6792
+ );
6793
+ phase = "replay-run";
6794
+ const result = await page.evaluate(async (serializedCapture) => {
6795
+ const host = globalThis.__forgeaxToolReplayHost;
6796
+ const value2 = await host.run(JSON.parse(serializedCapture));
6797
+ return JSON.parse(
6798
+ JSON.stringify(
6799
+ value2,
6800
+ (_key, nested2) => nested2 instanceof Error ? {
6801
+ ...nested2,
6802
+ name: nested2.name,
6803
+ message: nested2.message,
6804
+ stack: nested2.stack
6805
+ } : nested2
6806
+ )
6807
+ );
6808
+ }, JSON.stringify(capturedResult));
6809
+ await Promise.all(responseDiagnostics);
6810
+ if (!result.ok) return browserFailure("replay-run", result.error, pageErrors);
6811
+ if (pageErrors.length > 0)
6812
+ return browserFailure("replay-page-runtime", pageErrors[0], pageErrors);
6813
+ const endedAtMs = performance.now();
6814
+ const phases = result.result.operationTiming.phases;
6815
+ const observedDurationMs = phases === void 0 ? 0 : Object.values(phases).reduce(
6816
+ (total, observation) => total + (observation.status === "observed" ? observation.durationMs : 0),
6817
+ 0
6818
+ );
6819
+ const durationMs = endedAtMs - hostStartedAtMs;
6820
+ const value = {
6821
+ ...result.result,
6822
+ operationTiming: {
6823
+ ...result.result.operationTiming,
6824
+ startedAtMs: hostStartedAtMs,
6825
+ endedAtMs,
6826
+ durationMs,
6827
+ ...phases === void 0 ? { unattributedMs: durationMs } : {
6828
+ phases: {
6829
+ ...phases,
6830
+ transport: {
6831
+ status: "observed",
6832
+ durationMs: Math.max(0, durationMs - observedDurationMs)
6833
+ }
6834
+ },
6835
+ unattributedMs: 0
6836
+ }
6837
+ },
6838
+ actualCarrier: recipe.presentation === "hidden" ? "headless-private" : "headed-private"
6839
+ };
6840
+ phase = "artifact-publish";
6841
+ return {
6842
+ ok: true,
6843
+ value: options.publish === false ? value : await publishPreviewArtifacts(projectRoot2, runId, value)
6844
+ };
6845
+ } catch (cause) {
6846
+ await Promise.all(responseDiagnostics);
6847
+ return browserFailure(phase, cause, pageErrors);
6848
+ } finally {
6849
+ signal.removeEventListener("abort", abort);
6850
+ await closeBrowserProcess();
6851
+ await server.close().catch(() => void 0);
6852
+ await rm(cacheDir, { recursive: true, force: true });
6853
+ }
6854
+ }
6855
+ function runBrowserResourcePreviewHost(projectRoot2, recipe, snapshot, runId, signal, resource, options = {}) {
6856
+ return runBrowserPreviewHost(
6857
+ projectRoot2,
6858
+ recipe,
6859
+ snapshot,
6860
+ runId,
6861
+ signal,
6862
+ "resource-bootstrap",
6863
+ resource,
6864
+ options
6865
+ );
6866
+ }
6867
+ var init_browser_host = __esm({
6868
+ "src/tools/browser-host.ts"() {
6869
+ init_host();
6870
+ init_project();
6871
+ }
6872
+ });
6873
+
6874
+ // src/tools/native-preview.ts
6875
+ var native_preview_exports = {};
6876
+ __export(native_preview_exports, {
6877
+ isNativePreviewTool: () => isNativePreviewTool,
6878
+ runNativePreviewTool: () => runNativePreviewTool
6879
+ });
6880
+ function isNativePreviewTool(id) {
6881
+ return previewToolIds.has(id);
6882
+ }
6883
+ async function runNativePreviewTool(contribution, args2, options, projectRoot2) {
6884
+ const kind = contribution.descriptor.id.split(".")[0];
6885
+ if (kind !== "material" && kind !== "mesh" && kind !== "texture" && kind !== "vfx") {
6886
+ throw new Error(`unsupported native preview operation ${contribution.descriptor.id}`);
6887
+ }
6888
+ const parsed = contribution.descriptor.argsSchema.parse(args2);
6889
+ if (!parsed.ok) {
6890
+ return {
6891
+ outcome: "failed",
6892
+ failure: {
6893
+ code: "tool-invalid-args",
6894
+ expected: "preview arguments to match the operation argsSchema",
6895
+ hint: "Pass { guid } and an optional power-of-two size from AssetRegistry catalog identity.",
6896
+ detail: { message: parsed.error, value: null }
6897
+ },
6898
+ artifacts: []
6899
+ };
6900
+ }
6901
+ const request = parsed.value;
6902
+ const { guid, size = RESOURCE_PREVIEW_DEFAULT_SIZE } = request;
6903
+ const selectedPreviewPlugin = nativePreviewPlugins.find(
6904
+ ([, plugin]) => plugin.tools.some(
6905
+ (tool) => tool.descriptor.id === contribution.descriptor.id
6906
+ )
6907
+ );
6908
+ if (selectedPreviewPlugin === void 0) {
6909
+ throw new Error(`native preview plugin does not export ${contribution.descriptor.id}`);
6910
+ }
6911
+ const selectedPluginName = selectedPreviewPlugin[0];
6912
+ const snapshot = options.snapshot ?? { revision: 0, digest: `sha256:project:${projectRoot2}` };
6913
+ const previewRunId = `${contribution.descriptor.id}:${crypto.randomUUID()}`;
6914
+ const browser = await runBrowserResourcePreviewHost(
6915
+ projectRoot2,
6916
+ createToolPreviewRecipe({
6917
+ presentation: "hidden",
6918
+ viewport: { width: size, height: size },
6919
+ frames: 32
6920
+ }),
6921
+ snapshot,
6922
+ previewRunId,
6923
+ options.signal ?? new AbortController().signal,
6924
+ { kind, guid, size },
6925
+ { publish: false }
6926
+ );
6927
+ if (!browser.ok || browser.value.resource === void 0) {
6928
+ const failure2 = browser.ok ? {
6929
+ code: "tool-preview-bootstrap-failed",
6930
+ expected: "the Browser resource bootstrap to return AssetRegistry owner facts",
6931
+ hint: "Inspect resource-bootstrap and retry after the owner publishes the GUID payload.",
6932
+ detail: { phase: "resource-owner" }
6933
+ } : browser.error;
6934
+ return {
6935
+ outcome: "failed",
6936
+ failure: {
6937
+ code: "tool-domain-failed",
6938
+ expected: failure2.expected ?? "the Browser resource bootstrap to succeed",
6939
+ hint: failure2.hint ?? "Inspect the Browser resource bootstrap failure and retry.",
6940
+ detail: {
6941
+ code: failure2.code,
6942
+ ...failure2.detail === void 0 ? {} : { payload: failure2.detail }
6943
+ }
6944
+ },
6945
+ artifacts: []
6946
+ };
6947
+ }
6948
+ const resource = browser.value.resource;
6949
+ const subjectDrawn = kind === "texture" ? browser.value.drawCalls > 0 : browser.value.drawCalls > 2;
6950
+ if (!subjectDrawn) {
6951
+ return {
6952
+ outcome: "failed",
6953
+ failure: {
6954
+ code: "tool-domain-failed",
6955
+ expected: "the captured resource frame to contain a subject draw in addition to canonical presentation passes",
6956
+ hint: "Inspect material readiness and the RHI tape before retrying the same GUID preview.",
6957
+ detail: {
6958
+ code: "tool-preview-subject-not-rendered",
6959
+ payload: { kind, drawCalls: browser.value.drawCalls }
6960
+ }
6961
+ },
6962
+ artifacts: []
6963
+ };
6964
+ }
6965
+ const ctx = new Context();
6966
+ try {
6967
+ const host = createNativePreviewHost({
6968
+ runId: `${contribution.descriptor.id}:native`,
6969
+ snapshot,
6970
+ projectRoot: projectRoot2,
6971
+ backend: "webgpu",
6972
+ signal: options.signal ?? new AbortController().signal,
6973
+ assets: {
6974
+ loadByGuid: async () => ({
6975
+ ok: true,
6976
+ value: resource.asset,
6977
+ ...resource.digest === void 0 ? {} : { digest: resource.digest },
6978
+ ...resource.ownerFacts === void 0 ? {} : { ownerFacts: resource.ownerFacts }
6979
+ })
6980
+ },
6981
+ renderer: {
6982
+ rendererReady: browser.value.trace.events.includes("renderer-created"),
6983
+ worldReady: browser.value.trace.events.includes("world-updated"),
6984
+ drawCalls: browser.value.drawCalls,
6985
+ nonBlackPixels: browser.value.nonBlackPixels,
6986
+ ...resource.observation === void 0 ? {} : { observation: resource.observation },
6987
+ ...kind === "vfx" && resource.observation !== void 0 ? {
6988
+ vfx: {
6989
+ dispatches: typeof resource.observation.dispatches === "number" ? resource.observation.dispatches : 0,
6990
+ indirectDraws: typeof resource.observation.indirectDraws === "number" ? resource.observation.indirectDraws : 0,
6991
+ subjectOutputs: typeof resource.observation.subjectOutputs === "number" ? resource.observation.subjectOutputs : 0
6992
+ }
6993
+ } : {},
6994
+ texture: { drawCalls: browser.value.drawCalls }
6995
+ },
6996
+ artifacts: browser.value.artifacts
6997
+ });
6998
+ const { loader } = await installCatalogLoader(
6999
+ ctx,
7000
+ createNativePreviewCatalog(host, selectedPluginName),
7001
+ "host"
7002
+ );
7003
+ await loader.root.update(
7004
+ projectPluginEntries(
7005
+ previewPluginEntries.filter(({ name }) => name === selectedPluginName),
7006
+ "host",
7007
+ "host"
7008
+ )
7009
+ );
7010
+ await loader.await();
7011
+ const nativeContribution = nativePreviewTools.find(
7012
+ (candidate) => candidate.descriptor.id === contribution.descriptor.id
7013
+ );
7014
+ if (nativeContribution === void 0) {
7015
+ throw new Error(`native preview plugin does not export ${contribution.descriptor.id}`);
7016
+ }
7017
+ const terminal = await createToolRuntime([nativeContribution]).run(nativeContribution, args2, {
7018
+ ...options,
7019
+ snapshot,
7020
+ capabilityResolver: createContextCapabilityResolver(ctx)
7021
+ }).terminal;
7022
+ if (terminal.outcome !== "succeeded") return terminal;
7023
+ const domainResult = terminal.result;
7024
+ let published;
7025
+ try {
7026
+ published = await publishPreviewArtifacts(projectRoot2, previewRunId, browser.value, {
7027
+ snapshot,
7028
+ subject: domainResult.subject,
7029
+ presentation: domainResult.presentation,
7030
+ oracle: domainResult.oracle
7031
+ });
7032
+ } catch (cause) {
7033
+ return {
7034
+ outcome: "failed",
7035
+ failure: {
7036
+ code: "tool-domain-failed",
7037
+ expected: "the preview report and all capture artifacts to publish atomically",
7038
+ hint: "Inspect the artifact manifest identity or digest failure and retry the same ToolRun.",
7039
+ detail: {
7040
+ code: "tool-artifact-manifest-invalid",
7041
+ payload: cause instanceof Error ? cause.message : String(cause)
7042
+ }
7043
+ },
7044
+ artifacts: []
7045
+ };
7046
+ }
7047
+ const report = published.manifest.artifacts.find((artifact) => artifact.role === "report");
7048
+ if (report === void 0) {
7049
+ throw new Error("resource preview publisher returned no report artifact");
7050
+ }
7051
+ return {
7052
+ ...terminal,
7053
+ result: {
7054
+ ...terminal.result,
7055
+ actualCarrier: browser.value.actualCarrier,
7056
+ report: {
7057
+ kind: "tool-result",
7058
+ digest: report.digest,
7059
+ uri: report.uri,
7060
+ mediaType: report.mediaType,
7061
+ sizeBytes: report.byteLength
7062
+ },
7063
+ artifacts: published.artifacts
7064
+ },
7065
+ artifacts: published.artifacts
7066
+ };
7067
+ } finally {
7068
+ await ctx.fiber.dispose();
7069
+ }
7070
+ }
7071
+ var previewPluginEntries, previewToolIds;
7072
+ var init_native_preview = __esm({
7073
+ "src/tools/native-preview.ts"() {
7074
+ init_browser_host();
7075
+ init_preview_catalog();
7076
+ previewPluginEntries = nativePreviewPlugins.map(([name]) => ({
7077
+ id: `forgeax-preview-${name.slice(name.lastIndexOf("/") + 1)}`,
7078
+ name,
7079
+ realm: "host"
7080
+ }));
7081
+ previewToolIds = new Set(nativePreviewTools.map(({ descriptor }) => descriptor.id));
7082
+ }
7083
+ });
4371
7084
 
4372
7085
  // src/cli-output.ts
4373
7086
  function agentOnboardingLines(value) {
@@ -4376,6 +7089,12 @@ function agentOnboardingLines(value) {
4376
7089
  if (onboarding === null || typeof onboarding !== "object" || Array.isArray(onboarding)) return [];
4377
7090
  const candidate = onboarding;
4378
7091
  const lines = Array.isArray(candidate.read) ? candidate.read.filter((path) => typeof path === "string").map((path) => `[forgeax] read: ${path}`) : [];
7092
+ const selection = candidate.templateSelection;
7093
+ if (selection !== null && typeof selection === "object" && !Array.isArray(selection)) {
7094
+ lines.push("[forgeax] template required: use game-3d for a 3D game; otherwise use empty");
7095
+ lines.push("[forgeax] create 3D: forgeax new <directory> --template game-3d");
7096
+ lines.push("[forgeax] create other: forgeax new <directory> --template empty");
7097
+ }
4379
7098
  if (typeof candidate.next?.cwd === "string" && Array.isArray(candidate.next.argv) && candidate.next.argv.every((part) => typeof part === "string")) {
4380
7099
  lines.push(`[forgeax] next cwd: ${candidate.next.cwd}`);
4381
7100
  lines.push(`[forgeax] next: ${candidate.next.argv.join(" ")}`);
@@ -4396,12 +7115,15 @@ function sdkUpdateLines(value) {
4396
7115
  ];
4397
7116
  }
4398
7117
  function renderForgeaxUsage() {
4399
- return "Usage: forgeax new [directory] [--template empty|game-default]\n forgeax <init|doctor|test|dev|build|package|serve|preview> [directory]\n forgeax package [directory] [--output release/game-web.zip]\n forgeax run <rhi.capture|rhi.summary|rhi.inspect> [options]\n forgeax run <operation-id> --input <request.json>\n forgeax asset <add|verify|inspect|list> [subject]\n forgeax shader check [path]\n forgeax plugin <install|uninstall> <module-or-id> [options]\n forgeax skill <install|verify> [--root directory]\n forgeax sdk install <directory> [--version VERSION]\n";
7118
+ return "Usage: forgeax new [directory] --template empty|game-3d\n forgeax <init|doctor|test|dev|build|package|serve|preview> [directory]\n forgeax capture [directory] [--backend auto|software|hardware] [--require-ui] [--deterministic] [--headless] [--output PATH]\n forgeax engine <status|doctor|unlink> [--root directory]\n forgeax engine use-local <engine-directory> [--root directory]\n forgeax package [directory] [--format web-zip|single-html] [--output PATH]\n forgeax run <rhi.capture|rhi.summary|rhi.inspect> [options]\n forgeax run <operation-id> --input <request.json>\n forgeax exec <program.mjs> [--json]\n forgeax asset <add|verify|inspect|list> [subject]\n forgeax shader check [path]\n forgeax plugin <install|uninstall> <module-or-id> [options]\n forgeax skill <install|verify> [--root directory]\n forgeax sdk install <directory> [--version VERSION]\n";
4400
7119
  }
4401
7120
 
4402
7121
  // src/cli.ts
4403
7122
  init_commands();
4404
7123
 
7124
+ // src/tools/commands.ts
7125
+ init_software_capture();
7126
+
4405
7127
  // src/tools/client.ts
4406
7128
  init_catalog();
4407
7129
  function decorateResourcePreviewTerminal(terminal) {
@@ -4464,7 +7186,13 @@ async function createToolClient(options) {
4464
7186
  const contribution = runtime.get(id);
4465
7187
  if (contribution === void 0) return missingTool(id);
4466
7188
  const binding = bindingById.get(id);
4467
- const terminal = binding !== void 0 && runProjectTool2 !== void 0 ? await runProjectTool2(binding, args2, runOptions) : realmDispatch === void 0 ? await runtime.run(contribution, args2, runOptions).terminal : await realmDispatch.run(id, args2, runOptions);
7189
+ const nativePreview = binding === void 0 && id.endsWith(".preview") ? await Promise.resolve().then(() => (init_native_preview(), native_preview_exports)) : void 0;
7190
+ const terminal = nativePreview?.isNativePreviewTool(id) === true ? await nativePreview.runNativePreviewTool(
7191
+ contribution,
7192
+ args2,
7193
+ runOptions,
7194
+ options.projectRoot
7195
+ ) : binding !== void 0 && runProjectTool2 !== void 0 ? await runProjectTool2(binding, args2, runOptions) : realmDispatch === void 0 ? await runtime.run(contribution, args2, runOptions).terminal : await realmDispatch.run(id, args2, runOptions);
4468
7196
  return decorateResourcePreviewTerminal(terminal);
4469
7197
  }
4470
7198
  };
@@ -4572,6 +7300,7 @@ async function execCommand(options) {
4572
7300
  };
4573
7301
  }
4574
7302
  const root = options.root ?? process.cwd();
7303
+ const browser = createBrowserCapture(root);
4575
7304
  try {
4576
7305
  const module = await import(pathToFileURL(resolve(root, options.program)).href);
4577
7306
  const program = module.default ?? module.run;
@@ -4579,7 +7308,8 @@ async function execCommand(options) {
4579
7308
  throw new TypeError("operation program must export a default function or named run function");
4580
7309
  }
4581
7310
  const client = await createToolClient({ projectRoot: root });
4582
- const value = await program(client);
7311
+ const context = { ...client, browser };
7312
+ const value = await program(context);
4583
7313
  if (!isSerializableValue(value)) {
4584
7314
  throw new TypeError("tool program returned a non-serializable live value");
4585
7315
  }
@@ -4594,6 +7324,8 @@ async function execCommand(options) {
4594
7324
  detail: { reason: cause instanceof Error ? cause.message : String(cause) }
4595
7325
  }
4596
7326
  };
7327
+ } finally {
7328
+ await browser.close();
4597
7329
  }
4598
7330
  }
4599
7331
 
@@ -4602,8 +7334,8 @@ init_types();
4602
7334
  var rawArgs = process.argv.slice(2);
4603
7335
  var args = [...rawArgs];
4604
7336
  var primary = args.shift();
4605
- var nested = primary === "asset" || primary === "shader" || primary === "plugin" || primary === "skill" || primary === "sdk" ? args.shift() : void 0;
4606
- var command = primary === "asset" || primary === "shader" || primary === "plugin" || primary === "skill" || primary === "sdk" ? `${primary}.${nested ?? ""}` : primary;
7337
+ var nested = primary === "asset" || primary === "shader" || primary === "plugin" || primary === "skill" || primary === "sdk" || primary === "engine" ? args.shift() : void 0;
7338
+ var command = primary === "asset" || primary === "shader" || primary === "plugin" || primary === "skill" || primary === "sdk" || primary === "engine" ? `${primary}.${nested ?? ""}` : primary;
4607
7339
  var json = args.includes("--json");
4608
7340
  var dryRun = args.includes("--dry-run");
4609
7341
  var noInstall = args.includes("--no-install");
@@ -4616,6 +7348,8 @@ var optionValueIndexes = new Set(
4616
7348
  [
4617
7349
  "--artifact",
4618
7350
  "--base",
7351
+ "--backend",
7352
+ "--browser",
4619
7353
  "--digest",
4620
7354
  "--id",
4621
7355
  "--realm",
@@ -4623,10 +7357,14 @@ var optionValueIndexes = new Set(
4623
7357
  "--out-dir",
4624
7358
  "--input",
4625
7359
  "--output",
7360
+ "--format",
4626
7361
  "--root",
4627
7362
  "--port",
4628
7363
  "--template",
7364
+ "--height",
4629
7365
  "--version",
7366
+ "--wait-ms",
7367
+ "--width",
4630
7368
  "--work-index"
4631
7369
  ].flatMap((name) => {
4632
7370
  const index = args.indexOf(name);
@@ -4646,6 +7384,11 @@ var commands = [
4646
7384
  "dev",
4647
7385
  "build",
4648
7386
  "package",
7387
+ "capture",
7388
+ "engine.status",
7389
+ "engine.use-local",
7390
+ "engine.unlink",
7391
+ "engine.doctor",
4649
7392
  "serve",
4650
7393
  "preview",
4651
7394
  "run",
@@ -4706,12 +7449,122 @@ async function run(value) {
4706
7449
  }
4707
7450
  case "package": {
4708
7451
  const output = option("--output");
7452
+ const format = option("--format");
7453
+ if (args.includes("--format") && format === void 0) {
7454
+ return {
7455
+ ok: false,
7456
+ error: {
7457
+ code: "cli-parse-error",
7458
+ expected: "--format to be followed by web-zip or single-html",
7459
+ hint: "Pass --format web-zip or --format single-html.",
7460
+ detail: { option: "--format", received: null }
7461
+ }
7462
+ };
7463
+ }
7464
+ if (format !== void 0 && format !== "web-zip" && format !== "single-html") {
7465
+ return {
7466
+ ok: false,
7467
+ error: {
7468
+ code: "cli-parse-error",
7469
+ expected: "--format to be web-zip or single-html",
7470
+ hint: "Use web-zip for HTTPS hosting or single-html for a self-contained file:// delivery.",
7471
+ detail: { option: "--format", received: format }
7472
+ }
7473
+ };
7474
+ }
4709
7475
  return packageCommand({
4710
7476
  root: projectRoot,
4711
7477
  json,
4712
- ...output === void 0 ? {} : { output }
7478
+ ...output === void 0 ? {} : { output },
7479
+ ...format === void 0 ? {} : { format }
7480
+ });
7481
+ }
7482
+ case "capture": {
7483
+ const parsedPort = parseProjectPortOption(option("--port"), args.includes("--port"));
7484
+ if (!parsedPort.ok) return parsedPort;
7485
+ const software = args.includes("--software");
7486
+ const requestedBackend = option("--backend");
7487
+ if (requestedBackend !== void 0 && requestedBackend !== "auto" && requestedBackend !== "software" && requestedBackend !== "hardware") {
7488
+ return {
7489
+ ok: false,
7490
+ error: {
7491
+ code: "cli-parse-error",
7492
+ expected: "--backend to be auto, software, or hardware",
7493
+ hint: "Use auto for a portable capture, or assert one browser rendering lane explicitly.",
7494
+ detail: { option: "--backend", received: requestedBackend }
7495
+ }
7496
+ };
7497
+ }
7498
+ if (software && requestedBackend !== void 0 && requestedBackend !== "software") {
7499
+ return {
7500
+ ok: false,
7501
+ error: {
7502
+ code: "cli-parse-error",
7503
+ expected: "--software and --backend to select the same capture lane",
7504
+ hint: "Use --backend software (or only the legacy --software flag).",
7505
+ detail: { software: true, backend: requestedBackend }
7506
+ }
7507
+ };
7508
+ }
7509
+ const parseInteger = (name, fallback, minimum, maximum) => {
7510
+ const raw = option(name);
7511
+ if (raw === void 0) return { ok: true, value: fallback };
7512
+ const parsed = Number(raw);
7513
+ return Number.isSafeInteger(parsed) && parsed >= minimum && parsed <= maximum ? { ok: true, value: parsed } : {
7514
+ ok: false,
7515
+ error: {
7516
+ code: "cli-parse-error",
7517
+ expected: `${name} to be an integer from ${minimum} to ${maximum}`,
7518
+ hint: "Pass a bounded deterministic capture value.",
7519
+ detail: { option: name, received: raw }
7520
+ }
7521
+ };
7522
+ };
7523
+ const width = parseInteger("--width", 1280, 64, 8192);
7524
+ if (!width.ok) return width;
7525
+ const height = parseInteger("--height", 720, 64, 8192);
7526
+ if (!height.ok) return height;
7527
+ const waitMs = parseInteger("--wait-ms", 4e3, 0, 12e4);
7528
+ if (!waitMs.ok) return waitMs;
7529
+ const output = option("--output");
7530
+ const browser = option("--browser");
7531
+ return browserCaptureCommand({
7532
+ root: projectRoot,
7533
+ json,
7534
+ backend: requestedBackend ?? (software ? "software" : "auto"),
7535
+ ...software ? { software: true } : {},
7536
+ width: width.value,
7537
+ height: height.value,
7538
+ waitMs: waitMs.value,
7539
+ requireUi: args.includes("--require-ui"),
7540
+ deterministic: args.includes("--deterministic"),
7541
+ ...args.includes("--headless") ? { headless: true } : {},
7542
+ ...parsedPort.value === void 0 ? {} : { port: parsedPort.value },
7543
+ ...output === void 0 ? {} : { output },
7544
+ ...browser === void 0 ? {} : { browser }
4713
7545
  });
4714
7546
  }
7547
+ case "engine.status":
7548
+ return engineStatusCommand({ root: scopedRoot});
7549
+ case "engine.use-local": {
7550
+ const path = positionals[0];
7551
+ if (path === void 0) {
7552
+ return {
7553
+ ok: false,
7554
+ error: {
7555
+ code: "cli-parse-error",
7556
+ expected: "forgeax engine use-local <engine-directory>",
7557
+ hint: "Pass the local Engine source checkout or SDK source directory.",
7558
+ detail: {}
7559
+ }
7560
+ };
7561
+ }
7562
+ return engineUseLocalCommand({ root: scopedRoot, path, dryRun, json });
7563
+ }
7564
+ case "engine.unlink":
7565
+ return engineUnlinkCommand({ root: scopedRoot, dryRun});
7566
+ case "engine.doctor":
7567
+ return engineDoctorCommand({ root: scopedRoot});
4715
7568
  case "preview": {
4716
7569
  const options = projectServerOptions();
4717
7570
  return options.ok ? previewCommand(options.value) : options;
@@ -4892,7 +7745,29 @@ async function run(value) {
4892
7745
  }
4893
7746
  };
4894
7747
  }
4895
- return runRhiDebugCommand(operation, { artifact, workIndex }, context);
7748
+ const fieldsOption = option("--fields");
7749
+ const fields = fieldsOption?.split(",").map((field) => field.trim()).filter((field) => field.length > 0);
7750
+ const allowedFields = /* @__PURE__ */ new Set(["bindings", "pipeline", "pixels"]);
7751
+ if (fields?.some((field) => !allowedFields.has(field))) {
7752
+ return {
7753
+ ok: false,
7754
+ error: {
7755
+ code: "cli-parse-error",
7756
+ expected: "--fields to contain pipeline, bindings, and/or pixels",
7757
+ hint: "Use a comma-separated subset such as --fields pipeline,bindings.",
7758
+ detail: { fields: fieldsOption }
7759
+ }
7760
+ };
7761
+ }
7762
+ return runRhiDebugCommand(
7763
+ operation,
7764
+ {
7765
+ artifact,
7766
+ workIndex,
7767
+ ...fields === void 0 ? {} : { fields }
7768
+ },
7769
+ context
7770
+ );
4896
7771
  }
4897
7772
  }
4898
7773
  }