@forgeax/engine-devkit 0.1.7 → 0.1.20

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 (40) hide show
  1. package/README.md +21 -2
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/__tests__/native-preview-dispatch.test.d.ts +2 -0
  4. package/dist/__tests__/native-preview-dispatch.test.d.ts.map +1 -0
  5. package/dist/__tests__/native-preview-subject-draw.unit.test.d.ts +2 -0
  6. package/dist/__tests__/native-preview-subject-draw.unit.test.d.ts.map +1 -0
  7. package/dist/__tests__/single-html-runtime.e2e.test.d.ts +2 -0
  8. package/dist/__tests__/single-html-runtime.e2e.test.d.ts.map +1 -0
  9. package/dist/__tests__/single-html.test.d.ts +2 -0
  10. package/dist/__tests__/single-html.test.d.ts.map +1 -0
  11. package/dist/bootstrap-commands.d.ts.map +1 -1
  12. package/dist/cli-output.d.ts.map +1 -1
  13. package/dist/cli.mjs +1777 -133
  14. package/dist/cli.mjs.map +1 -1
  15. package/dist/commands.d.ts.map +1 -1
  16. package/dist/dist.d.ts +2 -2
  17. package/dist/dist.d.ts.map +1 -1
  18. package/dist/host.d.ts +6 -1
  19. package/dist/host.d.ts.map +1 -1
  20. package/dist/index.d.ts +5 -3
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.mjs +2317 -1125
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/sdk-bootstrap.d.ts +6 -1
  25. package/dist/sdk-bootstrap.d.ts.map +1 -1
  26. package/dist/sdk-cli.mjs +33 -15
  27. package/dist/sdk-cli.mjs.map +1 -1
  28. package/dist/sdk.d.ts +1 -3
  29. package/dist/sdk.d.ts.map +1 -1
  30. package/dist/single-html.d.ts +43 -0
  31. package/dist/single-html.d.ts.map +1 -0
  32. package/dist/software-capture.d.ts +38 -0
  33. package/dist/software-capture.d.ts.map +1 -1
  34. package/dist/tools/client.d.ts.map +1 -1
  35. package/dist/tools/native-preview.d.ts.map +1 -1
  36. package/dist/tools/preview-contributions.d.ts +0 -5
  37. package/dist/tools/preview-contributions.d.ts.map +1 -1
  38. package/dist/types.d.ts +2 -0
  39. package/dist/types.d.ts.map +1 -1
  40. package/package.json +33 -31
package/dist/cli.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import { deflateRawSync } from 'zlib';
3
3
  import { createHash, randomUUID } from 'crypto';
4
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 } from 'path';
5
+ import { resolve, extname, dirname, basename, relative, isAbsolute, sep, join } from 'path';
6
6
  import { GameProjectSchema } from '@forgeax/engine-project';
7
7
  import { readFileSync, existsSync } from 'fs';
8
8
  import { createRequire } from 'module';
@@ -14,23 +14,25 @@ import { gltfImporter } from '@forgeax/engine-gltf';
14
14
  import { imageImporter } from '@forgeax/engine-image/image-importer';
15
15
  import { BUILTIN_MESH_ASSETS } from '@forgeax/engine-pack/builtin';
16
16
  import { scanInventory } from '@forgeax/engine-pack/scanner';
17
- 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';
18
18
  import { createMaterialPackCooker } from '@forgeax/engine-shader-compiler';
19
19
  import { createStandaloneRuntimeAssetBinding, ok, err } from '@forgeax/engine-types';
20
20
  import { createParticleCodeNativeCookerFromRoots } from '@forgeax/engine-vfx-compiler';
21
21
  import { pluginPack, reloadAssetHost } from '@forgeax/engine-vite-plugin-pack';
22
+ import { vitePluginRhiDebug } from '@forgeax/engine-vite-plugin-rhi-debug';
22
23
  import { forgeaxShader } from '@forgeax/engine-vite-plugin-shader';
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
30
  import { spawn, execFile } from 'child_process';
27
- import { tmpdir } from 'os';
28
31
  import { promisify } from 'util';
29
32
  import { decodeTape, openReplay, buildFrameModel, replayDeviceRequest, createRhiDebugError } from '@forgeax/engine-rhi-debug';
30
33
  import { rhi, createShaderModule } from '@forgeax/engine-rhi-webgpu';
31
- import { build, preview, createServer as createServer$1 } from 'vite';
32
34
  import { createServer } from 'net';
33
- import { FORGEAX_FRAME_SUBMITTED_DATASET } from '@forgeax/engine-app';
35
+ import { FORGEAX_FRAME_SUBMITTED_DATASET, createToolPreviewRecipe } from '@forgeax/engine-app';
34
36
  import { parseImage } from '@forgeax/engine-image/parse-image';
35
37
  import { chromium } from 'playwright';
36
38
  import { startVitest } from 'vitest/node';
@@ -38,9 +40,9 @@ import materialPreviewPlugin from '@forgeax/engine-preview/material';
38
40
  import meshPreviewPlugin from '@forgeax/engine-preview/mesh';
39
41
  import texturePreviewPlugin from '@forgeax/engine-preview/texture';
40
42
  import vfxPreviewPlugin from '@forgeax/engine-preview/vfx';
41
- 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';
42
44
  import { Context, isToolPlugin } from '@forgeax/engine-plugin';
43
- import { installCatalogLoader, projectPluginEntries } from '@forgeax/engine-plugin/loader';
45
+ import { installCatalogLoader, projectPluginEntries, createContextCapabilityResolver } from '@forgeax/engine-plugin/loader';
44
46
 
45
47
  var __defProp = Object.defineProperty;
46
48
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -148,6 +150,20 @@ function mediaType(path) {
148
150
  if (path.endsWith(".wasm")) return "application/wasm";
149
151
  if (path.endsWith(".png")) return "image/png";
150
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";
151
167
  return "application/octet-stream";
152
168
  }
153
169
  async function filesUnder(root, directory = root) {
@@ -836,6 +852,7 @@ var init_engine_binding = __esm({
836
852
  // src/host.ts
837
853
  var host_exports = {};
838
854
  __export(host_exports, {
855
+ createEngineWorkspaceResolverForProject: () => createEngineWorkspaceResolverForProject,
839
856
  createViteConfig: () => createViteConfig,
840
857
  devKitDdcRoots: () => devKitDdcRoots,
841
858
  ignoreDevKitCatalogPath: () => ignoreDevKitCatalogPath
@@ -1006,6 +1023,9 @@ async function createEngineWorkspaceResolver(projectRoot2) {
1006
1023
  }
1007
1024
  };
1008
1025
  }
1026
+ async function createEngineWorkspaceResolverForProject(projectRoot2) {
1027
+ return createEngineWorkspaceResolver(projectRoot2);
1028
+ }
1009
1029
  async function consumerEngineAliases(projectRoot2) {
1010
1030
  const root = resolve(projectRoot2, "node_modules", ".pnpm", "node_modules", "@forgeax");
1011
1031
  try {
@@ -1322,6 +1342,44 @@ async function previewOwnerFacts(assets, resource, payload) {
1322
1342
  return undefined;
1323
1343
  }
1324
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
+
1325
1383
  async function prepareProject(app) {
1326
1384
  await prepareAssetRegistry(app.assets);
1327
1385
  previewWorld = app.world;
@@ -1506,6 +1564,7 @@ await app.pluginContext.plugin(gameHostPlugin({
1506
1564
  ...(defaultSceneRoot === undefined ? {} : { defaultSceneRoot }),
1507
1565
  uiRoot: uiRoot instanceof HTMLElement ? uiRoot : document.body,
1508
1566
  setPointerLockAllowed: (allowed) => app.input?.setPointerLockAllowed?.(allowed),
1567
+ ...(import.meta.env.DEV ? { gameProjection } : {}),
1509
1568
  }));
1510
1569
  const { loader: pluginLoader } = await installCatalogLoader(app.pluginContext, pluginCatalog, 'engine');
1511
1570
  await pluginLoader.root.update(projectPluginEntries(pluginEntries, 'engine'));
@@ -1542,7 +1601,10 @@ if (query.has('forgeax-tool-replay')) {
1542
1601
  if (current === undefined) return current;
1543
1602
  if (resource?.kind !== 'vfx') {
1544
1603
  if (resource?.kind === 'material' || resource?.kind === 'mesh' || resource?.kind === 'texture') {
1545
- 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;
1546
1608
  if (resource.kind === 'mesh') {
1547
1609
  const asset = current.asset;
1548
1610
  const aabb = asset?.aabb;
@@ -1652,6 +1714,7 @@ if (query.has('forgeax-tool-replay')) {
1652
1714
  );
1653
1715
  if (!result.ok) throw result.error;
1654
1716
  const app = result.value;
1717
+ if (import.meta.env.DEV) exposeGameInspection(app);
1655
1718
  await prepareProject(app);
1656
1719
  ${bootstrapPlugin}
1657
1720
  app.start().unwrap();
@@ -1796,6 +1859,7 @@ async function createViteConfig(facts, command2, base = "/", options = {}) {
1796
1859
  const consumerAliases = await consumerEngineAliases(facts.root);
1797
1860
  const plugins = [
1798
1861
  ...engineWorkspaceResolver === void 0 ? [] : [engineWorkspaceResolver],
1862
+ ...command2 === "serve" && process.env.FORGEAX_ENGINE_RHI_DEBUG === "1" ? [vitePluginRhiDebug({ rootDir: facts.root })] : [],
1799
1863
  forgeaxShader(),
1800
1864
  pluginPack({
1801
1865
  roots,
@@ -1840,6 +1904,737 @@ var init_host = __esm({
1840
1904
  hostRequire = createRequire(import.meta.url);
1841
1905
  }
1842
1906
  });
1907
+ function errorResult(code, expected, hint, detail = {}) {
1908
+ return { ok: false, error: new SingleHtmlError(code, expected, hint, detail) };
1909
+ }
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
+ });
1915
+ }
1916
+ function normalizePath(value) {
1917
+ let decoded;
1918
+ try {
1919
+ decoded = decodeURIComponent(value.split(/[?#]/, 1)[0] ?? value);
1920
+ } catch {
1921
+ return void 0;
1922
+ }
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;
1929
+ }
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}`)
2026
+ );
2027
+ return suffix.length === 1 ? suffix[0]?.[1] : void 0;
2028
+ }
2029
+ function resourcePath(reference, basePath) {
2030
+ try {
2031
+ return normalizePath(new URL(reference, `https://forgeax.invalid/${basePath}`).pathname);
2032
+ } catch {
2033
+ return void 0;
2034
+ }
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);
2040
+ }
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
+ }
2050
+ }
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."
2101
+ );
2102
+ }
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 }
2109
+ );
2110
+ }
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."
2122
+ );
2123
+ }
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 }
2130
+ );
2131
+ }
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
+ });
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}`)
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
+ }
2353
+ }
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 })]);
2596
+ }
2597
+ }
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
+ };
2636
+ }
2637
+ });
1843
2638
 
1844
2639
  // src/types.ts
1845
2640
  function parseProjectPortOption(value, provided) {
@@ -2118,11 +2913,13 @@ var init_package = __esm({
2118
2913
  "@forgeax/engine-types": "workspace:*",
2119
2914
  "@forgeax/engine-vfx-compiler": "workspace:*",
2120
2915
  "@forgeax/engine-vite-plugin-pack": "workspace:*",
2916
+ "@forgeax/engine-vite-plugin-rhi-debug": "workspace:*",
2121
2917
  "@forgeax/engine-vite-plugin-shader": "workspace:*",
2122
2918
  jiti: "1.21.7",
2919
+ parse5: "7.3.0",
2123
2920
  playwright: "1.60.0",
2124
2921
  vite: "8.0.10",
2125
- vitest: "4.0.18",
2922
+ vitest: "4.1.11",
2126
2923
  webgpu: "^0.4.0"
2127
2924
  },
2128
2925
  devDependencies: {
@@ -2297,7 +3094,7 @@ var init_init = __esm({
2297
3094
  "@webgpu/types": "0.1.71",
2298
3095
  tsx: "4.23.1",
2299
3096
  typescript: "6.0.3",
2300
- vitest: "4.0.18"
3097
+ vitest: "4.1.11"
2301
3098
  };
2302
3099
  }
2303
3100
  });
@@ -2319,15 +3116,12 @@ async function findSdkContext() {
2319
3116
  const templates = new Map(
2320
3117
  manifest.templates.map((template) => [template.id, resolve(cursor, template.root)])
2321
3118
  );
2322
- const defaultTemplate = manifest.templates.find((template) => template.default);
2323
- if (defaultTemplate === void 0) throw new Error("sdk-default-template-missing");
2324
3119
  const store = resolve(cursor, "store", "pnpm");
2325
3120
  return {
2326
3121
  root: cursor,
2327
3122
  manifest,
2328
3123
  ...await readable(store) ? { store } : {},
2329
- templates,
2330
- defaultTemplate: defaultTemplate.id
3124
+ templates
2331
3125
  };
2332
3126
  }
2333
3127
  const parent = dirname(cursor);
@@ -2347,10 +3141,14 @@ function agentOnboarding(sdk, projectRoot2) {
2347
3141
  resolve(root, "skills", "forgeax-engine-sdk", "SKILL.md"),
2348
3142
  resolve(root, "skills", "forgeax-engine-sdk", "references", "feature-catalog.md")
2349
3143
  ],
2350
- next: projectRoot2 === void 0 ? {
2351
- cwd: root,
2352
- argv: ["node", "./bin/forgeax.mjs", "new", "../my-game"]
2353
- } : { cwd: root, argv: ["pnpm", "exec", "forgeax", "list", "--json"] }
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"] } }
2354
3152
  };
2355
3153
  }
2356
3154
  function sdkInitPath(sdk) {
@@ -2432,8 +3230,8 @@ async function requireSdkInitialization(sdk) {
2432
3230
  return { ok: true, value: state };
2433
3231
  }
2434
3232
  async function copyBootstrapInputs(sdk, root) {
2435
- const template = sdk.templates.get(sdk.defaultTemplate);
2436
- 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");
2437
3235
  for (const name of ["package.json", "pnpm-lock.yaml", "pnpm-workspace.yaml"]) {
2438
3236
  await cp(resolve(template, name), resolve(root, name));
2439
3237
  }
@@ -2625,13 +3423,13 @@ async function pathKind(path) {
2625
3423
  throw cause;
2626
3424
  }
2627
3425
  }
2628
- async function filesUnder2(root, directory = root) {
3426
+ async function filesUnder3(root, directory = root) {
2629
3427
  const files = [];
2630
3428
  for (const name of (await readdir(directory)).sort()) {
2631
3429
  const path = resolve(directory, name);
2632
3430
  const info = await lstat(path);
2633
3431
  if (info.isSymbolicLink()) throw new Error(`skill-source-symlink: ${path}`);
2634
- if (info.isDirectory()) files.push(...await filesUnder2(root, path));
3432
+ if (info.isDirectory()) files.push(...await filesUnder3(root, path));
2635
3433
  else if (info.isFile()) files.push(path);
2636
3434
  }
2637
3435
  return files;
@@ -2643,7 +3441,7 @@ async function skillRow(sourceRoot, id) {
2643
3441
  if (await pathKind(resolve(root, "SKILL.md")) !== "file") {
2644
3442
  throw new Error(`skill-entry-missing: ${id}/SKILL.md`);
2645
3443
  }
2646
- const files = await filesUnder2(root);
3444
+ const files = await filesUnder3(root);
2647
3445
  return {
2648
3446
  id,
2649
3447
  root: `skills/${id}`,
@@ -2660,8 +3458,8 @@ async function discoverProjectSkills(root) {
2660
3458
  return Promise.all(ids.map((id) => skillRow(sourceRoot, id)));
2661
3459
  }
2662
3460
  async function sameFiles(leftRoot, rightRoot) {
2663
- const left = await filesUnder2(leftRoot);
2664
- const right = await filesUnder2(rightRoot);
3461
+ const left = await filesUnder3(leftRoot);
3462
+ const right = await filesUnder3(rightRoot);
2665
3463
  const leftNames = left.map((path) => slash(relative(leftRoot, path)));
2666
3464
  const rightNames = right.map((path) => slash(relative(rightRoot, path)));
2667
3465
  if (JSON.stringify(leftNames) !== JSON.stringify(rightNames)) return false;
@@ -3085,8 +3883,8 @@ async function initCommand(options = {}) {
3085
3883
  const applied = await applyInitPlan(facts.value, plan.value, options);
3086
3884
  if (!applied.ok || options.dryRun === true) return applied;
3087
3885
  if (sdk !== void 0) {
3088
- const template = sdk.templates.get(sdk.defaultTemplate);
3089
- 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");
3090
3888
  await copyFile(
3091
3889
  resolve(template, "pnpm-lock.yaml"),
3092
3890
  resolve(facts.value.root, "pnpm-lock.yaml")
@@ -3154,7 +3952,18 @@ async function newCommand(options = {}) {
3154
3952
  }
3155
3953
  };
3156
3954
  }
3157
- 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;
3158
3967
  const template = sdk.templates.get(templateId);
3159
3968
  if (template === void 0) {
3160
3969
  return {
@@ -4072,28 +4881,32 @@ function observedBackend(runtime) {
4072
4881
  return isSoftwareAdapter(runtime) ? "software" : "hardware";
4073
4882
  }
4074
4883
  async function resolveBrowserExecutable(requested) {
4884
+ if (requested !== void 0 && requested.length > 0) {
4885
+ return await pathExists2(requested) ? requested : void 0;
4886
+ }
4075
4887
  const candidates = [
4076
- requested,
4077
4888
  process.env.FORGEAX_BROWSER_EXECUTABLE,
4078
4889
  "/opt/google/chrome-beta/chrome",
4079
4890
  "/usr/bin/google-chrome",
4080
4891
  "/usr/bin/google-chrome-stable",
4081
4892
  "/usr/bin/chromium",
4082
- "/usr/bin/chromium-browser"
4893
+ "/usr/bin/chromium-browser",
4894
+ typeof chromium.executablePath === "function" ? chromium.executablePath() : void 0
4083
4895
  ].filter((candidate) => candidate !== void 0 && candidate.length > 0);
4084
4896
  for (const candidate of candidates) {
4085
4897
  if (await pathExists2(candidate)) return candidate;
4086
4898
  }
4087
4899
  return void 0;
4088
4900
  }
4089
- function browserLaunchArgs(backend) {
4090
- const common = [
4091
- "--enable-unsafe-webgpu",
4092
- "--ignore-gpu-blocklist",
4093
- "--disable-gpu-driver-bug-workarounds",
4094
- "--force-color-profile=srgb",
4095
- "--force-device-scale-factor=1"
4096
- ];
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
+ }
4097
4910
  if (backend !== "software") return common;
4098
4911
  return [
4099
4912
  ...common,
@@ -4184,6 +4997,7 @@ async function runtimeWitness(page) {
4184
4997
  adapterError = String(cause);
4185
4998
  }
4186
4999
  const frameId = Number(document.documentElement.dataset[datasetKey]);
5000
+ const singleHtmlState = globalThis.__forgeaxSingleHtml;
4187
5001
  return {
4188
5002
  title: document.title,
4189
5003
  canvas: canvas instanceof HTMLCanvasElement ? { width: canvas.width, height: canvas.height } : null,
@@ -4196,6 +5010,7 @@ async function runtimeWitness(page) {
4196
5010
  adapterError,
4197
5011
  engineFrameId: Number.isSafeInteger(frameId) && frameId > 0 ? frameId : null,
4198
5012
  captureReady: document.documentElement.dataset.forgeaxCaptureReady ?? null,
5013
+ singleHtml: singleHtmlState?.witness?.() ?? null,
4199
5014
  userAgent: navigator.userAgent
4200
5015
  };
4201
5016
  }, FORGEAX_FRAME_SUBMITTED_DATASET);
@@ -4211,8 +5026,81 @@ async function writeRunReport(report) {
4211
5026
  await writeFile(report.report, `${JSON.stringify(report, null, 2)}
4212
5027
  `, "utf8");
4213
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
+ }
4214
5078
  async function openBrowserCaptureSession(root, options) {
4215
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
+ }
4216
5104
  if (options.software === true && options.backend !== void 0 && options.backend !== "software") {
4217
5105
  fail(
4218
5106
  "browser-capture-option-conflict",
@@ -4250,27 +5138,32 @@ async function openBrowserCaptureSession(root, options) {
4250
5138
  facts.value.root,
4251
5139
  options.report ?? resolve(outputDirectory, "run.json")
4252
5140
  );
4253
- const port = options.port === void 0 || options.port === 0 ? await reservePort() : options.port;
5141
+ const port = target.kind === "project" ? options.port === void 0 || options.port === 0 ? await reservePort() : options.port : void 0;
4254
5142
  let server;
4255
5143
  let browser;
4256
5144
  let page;
4257
5145
  let display;
4258
5146
  try {
4259
- const { createServer: createServer2 } = await import('vite');
4260
- server = await createServer2(
4261
- await createViteConfig(facts.value, "serve", "/", {
4262
- server: { port, strictPort: true }
4263
- })
4264
- );
4265
- await server.listen(port);
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
+ }
4266
5157
  display = await startVirtualDisplay(width, height);
4267
5158
  const captureDisplay = display;
4268
5159
  const icd = await lavapipeIcd();
4269
5160
  const consoleErrors = [];
4270
5161
  const pageErrors = [];
5162
+ const requestRecords = [];
5163
+ const requestByObject = /* @__PURE__ */ new Map();
4271
5164
  const hasDisplay = display.value !== void 0;
4272
5165
  const headless = options.headless ?? (!hasDisplay && process.platform !== "linux");
4273
- const baseUrl = new URL(server.resolvedUrls?.local[0] ?? `http://127.0.0.1:${port}/`);
5166
+ const baseUrl = target.kind === "project" ? new URL(server?.resolvedUrls?.local[0] ?? `http://127.0.0.1:${port ?? 0}/`) : void 0;
4274
5167
  const openPage = async (launchBackend) => {
4275
5168
  const browserEnvironment = {
4276
5169
  ...Object.fromEntries(
@@ -4284,7 +5177,7 @@ async function openBrowserCaptureSession(root, options) {
4284
5177
  const launchOptions = {
4285
5178
  headless,
4286
5179
  env: browserEnvironment,
4287
- args: browserLaunchArgs(launchBackend),
5180
+ args: browserLaunchArgs(launchBackend, launchProfile),
4288
5181
  ...browserPath === void 0 ? {} : { executablePath: browserPath }
4289
5182
  };
4290
5183
  browser = await chromium.launch(launchOptions);
@@ -4301,7 +5194,14 @@ async function openBrowserCaptureSession(root, options) {
4301
5194
  if (message.type() === "error") consoleErrors.push(message.text());
4302
5195
  });
4303
5196
  page.on("pageerror", (error) => pageErrors.push(String(error)));
4304
- const captureUrl2 = new URL(baseUrl.href);
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));
4305
5205
  if (options.deterministic === true) captureUrl2.searchParams.set("forgeaxCapture", "1");
4306
5206
  await page.goto(captureUrl2.href, { waitUntil: "domcontentloaded", timeout: 12e4 });
4307
5207
  await page.waitForFunction(
@@ -4327,10 +5227,12 @@ async function openBrowserCaptureSession(root, options) {
4327
5227
  page = void 0;
4328
5228
  consoleErrors.length = 0;
4329
5229
  pageErrors.length = 0;
5230
+ requestRecords.length = 0;
5231
+ requestByObject.clear();
4330
5232
  runtime = await openPage("software");
4331
5233
  observed = observedBackend(runtime);
4332
5234
  }
4333
- const captureUrl = new URL(baseUrl.href);
5235
+ const captureUrl = target.kind === "single-html" ? new URL(pathToFileURL(target.path).href) : new URL(baseUrl?.href ?? "http://127.0.0.1/");
4334
5236
  if (options.deterministic === true) captureUrl.searchParams.set("forgeaxCapture", "1");
4335
5237
  if (page === void 0 || browser === void 0) {
4336
5238
  fail(
@@ -4340,6 +5242,7 @@ async function openBrowserCaptureSession(root, options) {
4340
5242
  );
4341
5243
  }
4342
5244
  const captures = [];
5245
+ let latestRuntime = runtime;
4343
5246
  let closed = false;
4344
5247
  const report = {
4345
5248
  schemaVersion: "2.0.0",
@@ -4348,6 +5251,8 @@ async function openBrowserCaptureSession(root, options) {
4348
5251
  mode: "browser-compositor",
4349
5252
  root: facts.value.root,
4350
5253
  url: captureUrl.href,
5254
+ target,
5255
+ launchProfile,
4351
5256
  report: reportPath,
4352
5257
  backendRequested: backend,
4353
5258
  backend: observed,
@@ -4366,14 +5271,19 @@ async function openBrowserCaptureSession(root, options) {
4366
5271
  display: display.value ?? null,
4367
5272
  lavapipeIcd: icd ?? null,
4368
5273
  captures,
5274
+ requests: summarizeRequests(requestRecords),
5275
+ singleHtml: runtime.singleHtml,
4369
5276
  consoleErrors,
4370
5277
  pageErrors,
4371
5278
  boundary: "Browser-compositor capture is visual iteration evidence, not physical-GPU performance, HDR-display output, or release acceptance."
4372
5279
  };
4373
5280
  await writeRunReport(report);
4374
5281
  const updateReport = async () => {
5282
+ const requests = summarizeRequests(requestRecords);
4375
5283
  Object.assign(report, {
4376
- ok: captures.length > 0 && captures.every((capture) => capture.ok) && consoleErrors.length === 0 && pageErrors.length === 0
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)
4377
5287
  });
4378
5288
  await writeRunReport(report);
4379
5289
  };
@@ -4438,12 +5348,14 @@ async function openBrowserCaptureSession(root, options) {
4438
5348
  }
4439
5349
  }
4440
5350
  const runtime2 = await runtimeWitness(activePage);
5351
+ latestRuntime = runtime2;
5352
+ const requestSummary = summarizeRequests(requestRecords);
4441
5353
  const uiPresent = runtime2.domUi.rootChildren > 0;
4442
5354
  const requireUi = captureOptions.requireUi ?? options.requireUi ?? false;
4443
5355
  const captureBackend = observedBackend(runtime2);
4444
5356
  if (captureBackend !== "unknown") Object.assign(report, { backend: captureBackend });
4445
5357
  const backendMatches = backend === "auto" || captureBackend === backend;
4446
- const ok2 = runtime2.canvas !== null && captured.pixels.rendered && backendMatches && runtime2.engineFrameId !== null && consoleErrors.length === 0 && pageErrors.length === 0 && (expectedReady === void 0 || runtime2.captureReady === expectedReady) && (!requireUi || uiPresent);
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);
4447
5359
  const index = captures.length + 1;
4448
5360
  const output = resolve(
4449
5361
  facts.value.root,
@@ -4649,31 +5561,47 @@ function releaseSlug(name) {
4649
5561
  async function packageCommand(options = {}) {
4650
5562
  const facts = await readProjectFacts(options.root);
4651
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
+ }
4652
5585
  const built = await buildCommand({
4653
5586
  root: facts.value.root,
4654
5587
  base: "./",
4655
5588
  ...options.json === void 0 ? {} : { json: options.json }
4656
5589
  });
4657
5590
  if (!built.ok) return built;
4658
- const distRoot = resolve(facts.value.root, "dist");
4659
5591
  const verified = await verifyDist(distRoot);
4660
5592
  if (!verified.ok) return verified;
4661
- const archive = resolve(
4662
- facts.value.root,
4663
- options.output ?? `release/${releaseSlug(facts.value.name)}-web.zip`
4664
- );
4665
- const archiveRelativeToDist = relative(distRoot, archive);
4666
- if (archiveRelativeToDist === "" || !archiveRelativeToDist.startsWith("..")) {
4667
- return {
4668
- ok: false,
4669
- error: {
4670
- code: "release-output-inside-dist",
4671
- expected: "the release archive to live outside the derived dist directory",
4672
- hint: "Use --output release/<game>-web.zip or another path outside dist.",
4673
- detail: { archive, distRoot }
4674
- }
4675
- };
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
+ });
4676
5603
  }
5604
+ const archive = output;
4677
5605
  const temporaryArchive = `${archive}.partial-${process.pid}`;
4678
5606
  const checksumPath = `${archive}.sha256`;
4679
5607
  const temporaryChecksum = `${checksumPath}.partial-${process.pid}`;
@@ -4686,11 +5614,11 @@ async function packageCommand(options = {}) {
4686
5614
  paths.map(async (path) => ({ path, bytes: await readFile(resolve(distRoot, path)) }))
4687
5615
  );
4688
5616
  const bytes = createZip(entries2);
4689
- const sha256 = createHash("sha256").update(bytes).digest("hex");
5617
+ const sha2562 = createHash("sha256").update(bytes).digest("hex");
4690
5618
  const manifestBytes = await readFile(resolve(distRoot, "forgeax-dist.json"));
4691
5619
  await mkdir(dirname(archive), { recursive: true });
4692
5620
  await writeFile(temporaryArchive, bytes);
4693
- await writeFile(temporaryChecksum, `${sha256} ${basename(archive)}
5621
+ await writeFile(temporaryChecksum, `${sha2562} ${basename(archive)}
4694
5622
  `);
4695
5623
  await rename(temporaryChecksum, checksumPath);
4696
5624
  await rename(temporaryArchive, archive);
@@ -4706,7 +5634,7 @@ async function packageCommand(options = {}) {
4706
5634
  delivery: "bundled-runtime",
4707
5635
  wasm: verified.value.artifacts.some((artifact) => artifact.path.endsWith(".wasm"))
4708
5636
  },
4709
- archive: { path: archive, bytes: bytes.byteLength, sha256 },
5637
+ archive: { path: archive, bytes: bytes.byteLength, sha256: sha2562 },
4710
5638
  checksumPath,
4711
5639
  distManifestSha256: createHash("sha256").update(manifestBytes).digest("hex"),
4712
5640
  run: {
@@ -4730,12 +5658,12 @@ async function devCommand(options = {}) {
4730
5658
  const previous = process.cwd();
4731
5659
  let server;
4732
5660
  try {
4733
- const { createServer: createServer2 } = await import('vite');
5661
+ const { createServer: createServer3 } = await import('vite');
4734
5662
  const { createViteConfig: createViteConfig2 } = await Promise.resolve().then(() => (init_host(), host_exports));
4735
5663
  const port = resolveProjectPort(options.port);
4736
5664
  const vitePort = { ...port, port: await materializeViteDevPort(port.port) };
4737
5665
  process.chdir(facts.value.root);
4738
- server = await createServer2(
5666
+ server = await createServer3(
4739
5667
  await createViteConfig2(facts.value, "serve", "/", {
4740
5668
  server: vitePort
4741
5669
  })
@@ -4843,6 +5771,7 @@ var init_commands = __esm({
4843
5771
  init_dist();
4844
5772
  init_host();
4845
5773
  init_project();
5774
+ init_single_html();
4846
5775
  init_types();
4847
5776
  init_assets();
4848
5777
  init_bootstrap_commands();
@@ -4857,6 +5786,19 @@ var init_commands = __esm({
4857
5786
  init_operations();
4858
5787
  }
4859
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
+ }
4860
5802
  var nativePreviewPlugins, nativePreviewTools, nativePreviewDescriptors;
4861
5803
  var init_preview_catalog = __esm({
4862
5804
  "src/tools/preview-catalog.ts"() {
@@ -5331,11 +6273,11 @@ async function runProjectTool(binding, args2, options) {
5331
6273
  );
5332
6274
  await loader.root.update(entries2);
5333
6275
  await loader.await();
5334
- const { createToolRuntime: createToolRuntime4 } = await import('@forgeax/engine-tool-runtime');
5335
- const { createContextCapabilityResolver } = await import('@forgeax/engine-plugin');
5336
- 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, {
5337
6279
  ...options,
5338
- capabilityResolver: createContextCapabilityResolver(ctx)
6280
+ capabilityResolver: createContextCapabilityResolver2(ctx)
5339
6281
  }).terminal;
5340
6282
  } catch (cause) {
5341
6283
  return activationFailure(cause);
@@ -5347,6 +6289,50 @@ var init_project_tools = __esm({
5347
6289
  "src/tools/project-tools.ts"() {
5348
6290
  }
5349
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
+ });
5350
6336
  function analyzePreviewArtifacts(request) {
5351
6337
  const required = request.required ?? [];
5352
6338
  const validated = request.manifest.schemaVersion === "1.0.0" ? required.some((kind) => kind !== "rhi-tape" && kind !== "png" && kind !== "profile-capture") ? {
@@ -5357,7 +6343,7 @@ function analyzePreviewArtifacts(request) {
5357
6343
  hint: "Request one of rhi-tape, png, or profile-capture for a v1 manifest.",
5358
6344
  detail: { reason: "unsupported required evidence kind" }
5359
6345
  }
5360
- } : validateArtifactManifest(request.manifest, required) : validatePreviewArtifactManifest(
6346
+ } : validateArtifactManifest(request.manifest, required) : validatePreviewArtifactManifest$1(
5361
6347
  request.manifest,
5362
6348
  (request.required ?? []).filter(
5363
6349
  (role) => role === "report" || role === "rhi-tape" || role === "capture" || role === "fresh-replay" || role === "profile-capture" || role === "contact-sheet"
@@ -5397,14 +6383,6 @@ function createPreviewContributions() {
5397
6383
  createOfflineAnalysisContribution()
5398
6384
  ];
5399
6385
  }
5400
- function createDomainPreviewContributions() {
5401
- return [
5402
- createMaterialPreviewContribution(),
5403
- createMeshPreviewContribution(),
5404
- createVfxPreviewContribution(),
5405
- createTexturePreviewContribution()
5406
- ];
5407
- }
5408
6386
  var init_preview_contributions = __esm({
5409
6387
  "src/tools/preview-contributions.ts"() {
5410
6388
  init_catalog();
@@ -5413,50 +6391,6 @@ var init_preview_contributions = __esm({
5413
6391
  }
5414
6392
  });
5415
6393
 
5416
- // src/tools/contributions.ts
5417
- var contributions_exports = {};
5418
- __export(contributions_exports, {
5419
- createAuthorContribution: () => createAuthorContribution,
5420
- createBuildContribution: () => createBuildContribution,
5421
- createDefaultContributions: () => createDefaultContributions
5422
- });
5423
- function commandFailure2(error) {
5424
- return { ok: false, error: { ...error, detail: error.detail } };
5425
- }
5426
- function createBuildContribution(projectRoot2 = process.cwd()) {
5427
- return defineTool(
5428
- projectBuildDescriptor,
5429
- async (options) => {
5430
- const { buildCommand: buildCommand2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
5431
- const result = await buildCommand2({ ...options, root: options.root ?? projectRoot2 });
5432
- return result.ok ? result.value : commandFailure2(result.error);
5433
- }
5434
- );
5435
- }
5436
- function createAuthorContribution(projectRoot2 = process.cwd()) {
5437
- return defineTool(
5438
- authorPluginInstallDescriptor,
5439
- async (options) => {
5440
- const { pluginInstallCommand: pluginInstallCommand2 } = await Promise.resolve().then(() => (init_plugin_authoring(), plugin_authoring_exports));
5441
- const result = await pluginInstallCommand2({ ...options, root: options.root ?? projectRoot2 });
5442
- return result.ok ? result.value : commandFailure2(result.error);
5443
- }
5444
- );
5445
- }
5446
- function createDefaultContributions(projectRoot2 = process.cwd()) {
5447
- return [
5448
- createBuildContribution(projectRoot2),
5449
- createAuthorContribution(projectRoot2),
5450
- ...createDomainPreviewContributions()
5451
- ];
5452
- }
5453
- var init_contributions = __esm({
5454
- "src/tools/contributions.ts"() {
5455
- init_catalog();
5456
- init_preview_contributions();
5457
- }
5458
- });
5459
-
5460
6394
  // src/tools/runtime.ts
5461
6395
  var runtime_exports = {};
5462
6396
  __export(runtime_exports, {
@@ -5474,6 +6408,679 @@ var init_runtime = __esm({
5474
6408
  init_preview_contributions();
5475
6409
  }
5476
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
+ });
5477
7084
 
5478
7085
  // src/cli-output.ts
5479
7086
  function agentOnboardingLines(value) {
@@ -5482,6 +7089,12 @@ function agentOnboardingLines(value) {
5482
7089
  if (onboarding === null || typeof onboarding !== "object" || Array.isArray(onboarding)) return [];
5483
7090
  const candidate = onboarding;
5484
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
+ }
5485
7098
  if (typeof candidate.next?.cwd === "string" && Array.isArray(candidate.next.argv) && candidate.next.argv.every((part) => typeof part === "string")) {
5486
7099
  lines.push(`[forgeax] next cwd: ${candidate.next.cwd}`);
5487
7100
  lines.push(`[forgeax] next: ${candidate.next.argv.join(" ")}`);
@@ -5502,7 +7115,7 @@ function sdkUpdateLines(value) {
5502
7115
  ];
5503
7116
  }
5504
7117
  function renderForgeaxUsage() {
5505
- return "Usage: forgeax new [directory] [--template empty|game-3d]\n forgeax <init|doctor|test|dev|build|package|serve|preview> [directory]\n forgeax capture [directory] [--backend auto|software|hardware] [--require-ui] [--deterministic] [--headless] [--output PATH]\n forgeax engine <status|doctor|unlink> [--root directory]\n forgeax engine use-local <engine-directory> [--root directory]\n forgeax package [directory] [--output release/game-web.zip]\n forgeax run <rhi.capture|rhi.summary|rhi.inspect> [options]\n forgeax run <operation-id> --input <request.json>\n forgeax exec <program.mjs> [--json]\n forgeax asset <add|verify|inspect|list> [subject]\n forgeax shader check [path]\n forgeax plugin <install|uninstall> <module-or-id> [options]\n forgeax skill <install|verify> [--root directory]\n forgeax sdk install <directory> [--version VERSION]\n";
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";
5506
7119
  }
5507
7120
 
5508
7121
  // src/cli.ts
@@ -5573,7 +7186,13 @@ async function createToolClient(options) {
5573
7186
  const contribution = runtime.get(id);
5574
7187
  if (contribution === void 0) return missingTool(id);
5575
7188
  const binding = bindingById.get(id);
5576
- 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);
5577
7196
  return decorateResourcePreviewTerminal(terminal);
5578
7197
  }
5579
7198
  };
@@ -5738,6 +7357,7 @@ var optionValueIndexes = new Set(
5738
7357
  "--out-dir",
5739
7358
  "--input",
5740
7359
  "--output",
7360
+ "--format",
5741
7361
  "--root",
5742
7362
  "--port",
5743
7363
  "--template",
@@ -5829,10 +7449,34 @@ async function run(value) {
5829
7449
  }
5830
7450
  case "package": {
5831
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
+ }
5832
7475
  return packageCommand({
5833
7476
  root: projectRoot,
5834
7477
  json,
5835
- ...output === void 0 ? {} : { output }
7478
+ ...output === void 0 ? {} : { output },
7479
+ ...format === void 0 ? {} : { format }
5836
7480
  });
5837
7481
  }
5838
7482
  case "capture": {