@forgeax/engine-devkit 0.1.2
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.
- package/LICENSE +15 -0
- package/README.md +192 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/archive.test.d.ts +2 -0
- package/dist/__tests__/archive.test.d.ts.map +1 -0
- package/dist/__tests__/assets.test.d.ts +2 -0
- package/dist/__tests__/assets.test.d.ts.map +1 -0
- package/dist/__tests__/bootstrap-commands.test.d.ts +2 -0
- package/dist/__tests__/bootstrap-commands.test.d.ts.map +1 -0
- package/dist/__tests__/dist.test.d.ts +2 -0
- package/dist/__tests__/dist.test.d.ts.map +1 -0
- package/dist/__tests__/host.test.d.ts +2 -0
- package/dist/__tests__/host.test.d.ts.map +1 -0
- package/dist/__tests__/init.test.d.ts +2 -0
- package/dist/__tests__/init.test.d.ts.map +1 -0
- package/dist/__tests__/plugin-authoring.test.d.ts +2 -0
- package/dist/__tests__/plugin-authoring.test.d.ts.map +1 -0
- package/dist/__tests__/project.test.d.ts +2 -0
- package/dist/__tests__/project.test.d.ts.map +1 -0
- package/dist/__tests__/rhi-debug-cold-start.integration.test.d.ts +2 -0
- package/dist/__tests__/rhi-debug-cold-start.integration.test.d.ts.map +1 -0
- package/dist/__tests__/rhi-debug-error-consumer.test-d.d.ts +2 -0
- package/dist/__tests__/rhi-debug-error-consumer.test-d.d.ts.map +1 -0
- package/dist/__tests__/rhi-debug-operations.test-d.d.ts +2 -0
- package/dist/__tests__/rhi-debug-operations.test-d.d.ts.map +1 -0
- package/dist/__tests__/rhi-debug-operations.unit.test.d.ts +2 -0
- package/dist/__tests__/rhi-debug-operations.unit.test.d.ts.map +1 -0
- package/dist/__tests__/sdk-install.test.d.ts +2 -0
- package/dist/__tests__/sdk-install.test.d.ts.map +1 -0
- package/dist/__tests__/skill-install.test.d.ts +2 -0
- package/dist/__tests__/skill-install.test.d.ts.map +1 -0
- package/dist/__tests__/tool-migration.test.d.ts +2 -0
- package/dist/__tests__/tool-migration.test.d.ts.map +1 -0
- package/dist/archive.d.ts +6 -0
- package/dist/archive.d.ts.map +1 -0
- package/dist/assets.d.ts +6 -0
- package/dist/assets.d.ts.map +1 -0
- package/dist/bootstrap-commands.d.ts +12 -0
- package/dist/bootstrap-commands.d.ts.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.mjs +4862 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/commands.d.ts +19 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/dist.d.ts +24 -0
- package/dist/dist.d.ts.map +1 -0
- package/dist/host/base-host.d.ts +18 -0
- package/dist/host/base-host.d.ts.map +1 -0
- package/dist/host/project-bootstrap.d.ts +11 -0
- package/dist/host/project-bootstrap.d.ts.map +1 -0
- package/dist/host/resource-bootstrap.d.ts +23 -0
- package/dist/host/resource-bootstrap.d.ts.map +1 -0
- package/dist/host.d.ts +13 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +5762 -0
- package/dist/index.mjs.map +1 -0
- package/dist/init.d.ts +20 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/plugin-authoring.d.ts +4 -0
- package/dist/plugin-authoring.d.ts.map +1 -0
- package/dist/project.d.ts +5 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/rhi-debug/cli-context.d.ts +3 -0
- package/dist/rhi-debug/cli-context.d.ts.map +1 -0
- package/dist/rhi-debug/operations.d.ts +83 -0
- package/dist/rhi-debug/operations.d.ts.map +1 -0
- package/dist/sdk-cli.d.ts +2 -0
- package/dist/sdk-cli.d.ts.map +1 -0
- package/dist/sdk-cli.mjs +5560 -0
- package/dist/sdk-cli.mjs.map +1 -0
- package/dist/sdk-install.d.ts +7 -0
- package/dist/sdk-install.d.ts.map +1 -0
- package/dist/sdk.d.ts +69 -0
- package/dist/sdk.d.ts.map +1 -0
- package/dist/shader-check.d.ts +3 -0
- package/dist/shader-check.d.ts.map +1 -0
- package/dist/skill-install.d.ts +16 -0
- package/dist/skill-install.d.ts.map +1 -0
- package/dist/tools/__tests__/browser-host-port.test.d.ts +2 -0
- package/dist/tools/__tests__/browser-host-port.test.d.ts.map +1 -0
- package/dist/tools/__tests__/carrier-faults.integration.test.d.ts +2 -0
- package/dist/tools/__tests__/carrier-faults.integration.test.d.ts.map +1 -0
- package/dist/tools/__tests__/carrier-provider.integration.test.d.ts +2 -0
- package/dist/tools/__tests__/carrier-provider.integration.test.d.ts.map +1 -0
- package/dist/tools/__tests__/commands.test.d.ts +2 -0
- package/dist/tools/__tests__/commands.test.d.ts.map +1 -0
- package/dist/tools/__tests__/coverage-gaps.test.d.ts +2 -0
- package/dist/tools/__tests__/coverage-gaps.test.d.ts.map +1 -0
- package/dist/tools/__tests__/domain-catalog.test.d.ts +2 -0
- package/dist/tools/__tests__/domain-catalog.test.d.ts.map +1 -0
- package/dist/tools/__tests__/preview-owner-gate.test.d.ts +2 -0
- package/dist/tools/__tests__/preview-owner-gate.test.d.ts.map +1 -0
- package/dist/tools/__tests__/preview-publication.test.d.ts +2 -0
- package/dist/tools/__tests__/preview-publication.test.d.ts.map +1 -0
- package/dist/tools/__tests__/realm-dispatch.integration.test.d.ts +2 -0
- package/dist/tools/__tests__/realm-dispatch.integration.test.d.ts.map +1 -0
- package/dist/tools/benchmark/__tests__/admission-fixtures.test.d.ts +2 -0
- package/dist/tools/benchmark/__tests__/admission-fixtures.test.d.ts.map +1 -0
- package/dist/tools/benchmark/harness.d.ts +17 -0
- package/dist/tools/benchmark/harness.d.ts.map +1 -0
- package/dist/tools/benchmark/index.d.ts +4 -0
- package/dist/tools/benchmark/index.d.ts.map +1 -0
- package/dist/tools/benchmark/report.d.ts +54 -0
- package/dist/tools/benchmark/report.d.ts.map +1 -0
- package/dist/tools/benchmark/statistics.d.ts +8 -0
- package/dist/tools/benchmark/statistics.d.ts.map +1 -0
- package/dist/tools/bootstrap.d.ts +49 -0
- package/dist/tools/bootstrap.d.ts.map +1 -0
- package/dist/tools/browser-host.d.ts +27 -0
- package/dist/tools/browser-host.d.ts.map +1 -0
- package/dist/tools/cache.d.ts +14 -0
- package/dist/tools/cache.d.ts.map +1 -0
- package/dist/tools/capability.d.ts +2 -0
- package/dist/tools/capability.d.ts.map +1 -0
- package/dist/tools/carrier-provider.d.ts +31 -0
- package/dist/tools/carrier-provider.d.ts.map +1 -0
- package/dist/tools/carrier-rendezvous.d.ts +16 -0
- package/dist/tools/carrier-rendezvous.d.ts.map +1 -0
- package/dist/tools/catalog.d.ts +66 -0
- package/dist/tools/catalog.d.ts.map +1 -0
- package/dist/tools/cli-adapter.d.ts +5 -0
- package/dist/tools/cli-adapter.d.ts.map +1 -0
- package/dist/tools/client.d.ts +18 -0
- package/dist/tools/client.d.ts.map +1 -0
- package/dist/tools/commands.d.ts +6 -0
- package/dist/tools/commands.d.ts.map +1 -0
- package/dist/tools/contributions.d.ts +6 -0
- package/dist/tools/contributions.d.ts.map +1 -0
- package/dist/tools/library.d.ts +3 -0
- package/dist/tools/library.d.ts.map +1 -0
- package/dist/tools/migration.d.ts +36 -0
- package/dist/tools/migration.d.ts.map +1 -0
- package/dist/tools/native-preview.d.ts +4 -0
- package/dist/tools/native-preview.d.ts.map +1 -0
- package/dist/tools/offline-analysis.d.ts +22 -0
- package/dist/tools/offline-analysis.d.ts.map +1 -0
- package/dist/tools/preview-catalog.d.ts +8 -0
- package/dist/tools/preview-catalog.d.ts.map +1 -0
- package/dist/tools/preview-contributions.d.ts +12 -0
- package/dist/tools/preview-contributions.d.ts.map +1 -0
- package/dist/tools/preview-host.d.ts +85 -0
- package/dist/tools/preview-host.d.ts.map +1 -0
- package/dist/tools/preview-migration.d.ts +4 -0
- package/dist/tools/preview-migration.d.ts.map +1 -0
- package/dist/tools/private-executor.d.ts +3 -0
- package/dist/tools/private-executor.d.ts.map +1 -0
- package/dist/tools/project-tools.d.ts +23 -0
- package/dist/tools/project-tools.d.ts.map +1 -0
- package/dist/tools/realms.d.ts +22 -0
- package/dist/tools/realms.d.ts.map +1 -0
- package/dist/tools/run-command.d.ts +17 -0
- package/dist/tools/run-command.d.ts.map +1 -0
- package/dist/tools/runtime.d.ts +5 -0
- package/dist/tools/runtime.d.ts.map +1 -0
- package/dist/tools/service-transport.d.ts +17 -0
- package/dist/tools/service-transport.d.ts.map +1 -0
- package/dist/tools/service.d.ts +22 -0
- package/dist/tools/service.d.ts.map +1 -0
- package/dist/types.d.ts +84 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +95 -0
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,4862 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { deflateRawSync } from 'zlib';
|
|
3
|
+
import { createHash } from 'crypto';
|
|
4
|
+
import { readFile, mkdtemp, rm, readdir, mkdir, cp, rename, writeFile, copyFile, stat, symlink, unlink, realpath, readlink, lstat, rmdir, access } from 'fs/promises';
|
|
5
|
+
import { resolve, extname, dirname, basename, relative, isAbsolute, sep } from 'path';
|
|
6
|
+
import { readFileSync, existsSync } from 'fs';
|
|
7
|
+
import { createRequire } from 'module';
|
|
8
|
+
import { pathToFileURL, fileURLToPath } from 'url';
|
|
9
|
+
import { audioImporter } from '@forgeax/engine-audio-webaudio/audio-importer';
|
|
10
|
+
import { fbxImporter } from '@forgeax/engine-fbx';
|
|
11
|
+
import { fontImporter } from '@forgeax/engine-font/font-importer';
|
|
12
|
+
import { gltfImporter } from '@forgeax/engine-gltf';
|
|
13
|
+
import { imageImporter } from '@forgeax/engine-image/image-importer';
|
|
14
|
+
import { describeResourcePreviewFailure, validateCanonicalKitReceipt, createMaterialPreviewContribution, createMeshPreviewContribution, createVfxPreviewContribution, createTexturePreviewContribution, validatePreviewArtifactManifest } from '@forgeax/engine-preview';
|
|
15
|
+
import { createStandaloneRuntimeAssetBinding, ok, err } from '@forgeax/engine-types';
|
|
16
|
+
import { createParticleCodeNativeCookerFromRoots } from '@forgeax/engine-vfx-compiler';
|
|
17
|
+
import { pluginPack, reloadAssetHost } from '@forgeax/engine-vite-plugin-pack';
|
|
18
|
+
import { forgeaxShader } from '@forgeax/engine-vite-plugin-shader';
|
|
19
|
+
import { GameProjectSchema } from '@forgeax/engine-project';
|
|
20
|
+
import { runCliGltf } from '@forgeax/engine-gltf/cli-gltf';
|
|
21
|
+
import { scanEntries } from '@forgeax/engine-pack/cli-asset';
|
|
22
|
+
import { AssetGuid } from '@forgeax/engine-pack/guid';
|
|
23
|
+
import { execFile } from 'child_process';
|
|
24
|
+
import { promisify } from 'util';
|
|
25
|
+
import { decodeTape, openReplay, buildFrameModel, createRhiDebugError } from '@forgeax/engine-rhi-debug';
|
|
26
|
+
import { tmpdir } from 'os';
|
|
27
|
+
import { build, createServer, preview } from 'vite';
|
|
28
|
+
import { startVitest } from 'vitest/node';
|
|
29
|
+
import materialPreviewPlugin from '@forgeax/engine-preview/material';
|
|
30
|
+
import meshPreviewPlugin from '@forgeax/engine-preview/mesh';
|
|
31
|
+
import texturePreviewPlugin from '@forgeax/engine-preview/texture';
|
|
32
|
+
import vfxPreviewPlugin from '@forgeax/engine-preview/vfx';
|
|
33
|
+
import { isSerializableValue, domainFailureError, createToolRuntime, capabilityUnavailableError, defineTool, validateArtifactManifest } from '@forgeax/engine-tool-runtime';
|
|
34
|
+
import { Context, isToolPlugin } from '@forgeax/engine-plugin';
|
|
35
|
+
import { installCatalogLoader, projectPluginEntries } from '@forgeax/engine-plugin/loader';
|
|
36
|
+
|
|
37
|
+
var __defProp = Object.defineProperty;
|
|
38
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
39
|
+
var __esm = (fn, res) => function __init() {
|
|
40
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
41
|
+
};
|
|
42
|
+
var __export = (target, all) => {
|
|
43
|
+
for (var name in all)
|
|
44
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
45
|
+
};
|
|
46
|
+
function crc32(bytes) {
|
|
47
|
+
let value = 4294967295;
|
|
48
|
+
for (const byte of bytes) value = value >>> 8 ^ (CRC_TABLE[(value ^ byte) & 255] ?? 0);
|
|
49
|
+
return (value ^ 4294967295) >>> 0;
|
|
50
|
+
}
|
|
51
|
+
function assertArchivePath(path) {
|
|
52
|
+
if (path.length === 0 || path.startsWith("/") || path.includes("\\") || path.split("/").some((segment) => segment.length === 0 || segment === "." || segment === "..")) {
|
|
53
|
+
throw new TypeError(`invalid ZIP entry path: ${path}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function createZip(entriesInput) {
|
|
57
|
+
if (entriesInput.length > ZIP_ENTRY_LIMIT) throw new RangeError("ZIP32 entry limit exceeded");
|
|
58
|
+
const entries2 = [...entriesInput].sort((left, right) => left.path.localeCompare(right.path));
|
|
59
|
+
const seen = /* @__PURE__ */ new Set();
|
|
60
|
+
const localParts = [];
|
|
61
|
+
const centralParts = [];
|
|
62
|
+
let offset = 0;
|
|
63
|
+
for (const entry of entries2) {
|
|
64
|
+
assertArchivePath(entry.path);
|
|
65
|
+
if (seen.has(entry.path)) throw new TypeError(`duplicate ZIP entry path: ${entry.path}`);
|
|
66
|
+
seen.add(entry.path);
|
|
67
|
+
const name = Buffer.from(entry.path, "utf8");
|
|
68
|
+
const source = Buffer.from(entry.bytes);
|
|
69
|
+
const compressed = deflateRawSync(source, { level: 9 });
|
|
70
|
+
if (source.byteLength > ZIP32_LIMIT || compressed.byteLength > ZIP32_LIMIT) {
|
|
71
|
+
throw new RangeError(`ZIP32 file limit exceeded: ${entry.path}`);
|
|
72
|
+
}
|
|
73
|
+
const checksum = crc32(source);
|
|
74
|
+
const local = Buffer.alloc(30);
|
|
75
|
+
local.writeUInt32LE(67324752, 0);
|
|
76
|
+
local.writeUInt16LE(20, 4);
|
|
77
|
+
local.writeUInt16LE(UTF8_FLAG, 6);
|
|
78
|
+
local.writeUInt16LE(DEFLATE_METHOD, 8);
|
|
79
|
+
local.writeUInt16LE(0, 10);
|
|
80
|
+
local.writeUInt16LE(DOS_EPOCH_DATE, 12);
|
|
81
|
+
local.writeUInt32LE(checksum, 14);
|
|
82
|
+
local.writeUInt32LE(compressed.byteLength, 18);
|
|
83
|
+
local.writeUInt32LE(source.byteLength, 22);
|
|
84
|
+
local.writeUInt16LE(name.byteLength, 26);
|
|
85
|
+
local.writeUInt16LE(0, 28);
|
|
86
|
+
localParts.push(local, name, compressed);
|
|
87
|
+
const central = Buffer.alloc(46);
|
|
88
|
+
central.writeUInt32LE(33639248, 0);
|
|
89
|
+
central.writeUInt16LE(20, 4);
|
|
90
|
+
central.writeUInt16LE(20, 6);
|
|
91
|
+
central.writeUInt16LE(UTF8_FLAG, 8);
|
|
92
|
+
central.writeUInt16LE(DEFLATE_METHOD, 10);
|
|
93
|
+
central.writeUInt16LE(0, 12);
|
|
94
|
+
central.writeUInt16LE(DOS_EPOCH_DATE, 14);
|
|
95
|
+
central.writeUInt32LE(checksum, 16);
|
|
96
|
+
central.writeUInt32LE(compressed.byteLength, 20);
|
|
97
|
+
central.writeUInt32LE(source.byteLength, 24);
|
|
98
|
+
central.writeUInt16LE(name.byteLength, 28);
|
|
99
|
+
central.writeUInt16LE(0, 30);
|
|
100
|
+
central.writeUInt16LE(0, 32);
|
|
101
|
+
central.writeUInt16LE(0, 34);
|
|
102
|
+
central.writeUInt16LE(0, 36);
|
|
103
|
+
central.writeUInt32LE(0, 38);
|
|
104
|
+
central.writeUInt32LE(offset, 42);
|
|
105
|
+
centralParts.push(central, name);
|
|
106
|
+
offset += local.byteLength + name.byteLength + compressed.byteLength;
|
|
107
|
+
if (offset > ZIP32_LIMIT) throw new RangeError("ZIP32 archive limit exceeded");
|
|
108
|
+
}
|
|
109
|
+
const centralDirectory = Buffer.concat(centralParts);
|
|
110
|
+
if (centralDirectory.byteLength > ZIP32_LIMIT)
|
|
111
|
+
throw new RangeError("ZIP32 directory limit exceeded");
|
|
112
|
+
const end = Buffer.alloc(22);
|
|
113
|
+
end.writeUInt32LE(101010256, 0);
|
|
114
|
+
end.writeUInt16LE(entries2.length, 8);
|
|
115
|
+
end.writeUInt16LE(entries2.length, 10);
|
|
116
|
+
end.writeUInt32LE(centralDirectory.byteLength, 12);
|
|
117
|
+
end.writeUInt32LE(offset, 16);
|
|
118
|
+
return Buffer.concat([...localParts, centralDirectory, end]);
|
|
119
|
+
}
|
|
120
|
+
var ZIP32_LIMIT, ZIP_ENTRY_LIMIT, UTF8_FLAG, DEFLATE_METHOD, DOS_EPOCH_DATE, CRC_TABLE;
|
|
121
|
+
var init_archive = __esm({
|
|
122
|
+
"src/archive.ts"() {
|
|
123
|
+
ZIP32_LIMIT = 4294967295;
|
|
124
|
+
ZIP_ENTRY_LIMIT = 65535;
|
|
125
|
+
UTF8_FLAG = 2048;
|
|
126
|
+
DEFLATE_METHOD = 8;
|
|
127
|
+
DOS_EPOCH_DATE = 33;
|
|
128
|
+
CRC_TABLE = new Uint32Array(256);
|
|
129
|
+
for (let index = 0; index < CRC_TABLE.length; index += 1) {
|
|
130
|
+
let value = index;
|
|
131
|
+
for (let bit = 0; bit < 8; bit += 1) value = value >>> 1 ^ 3988292384 & -(value & 1);
|
|
132
|
+
CRC_TABLE[index] = value >>> 0;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
function mediaType(path) {
|
|
137
|
+
if (path.endsWith(".html")) return "text/html";
|
|
138
|
+
if (path.endsWith(".js") || path.endsWith(".mjs")) return "text/javascript";
|
|
139
|
+
if (path.endsWith(".json")) return "application/json";
|
|
140
|
+
if (path.endsWith(".wasm")) return "application/wasm";
|
|
141
|
+
if (path.endsWith(".png")) return "image/png";
|
|
142
|
+
if (path.endsWith(".jpg") || path.endsWith(".jpeg")) return "image/jpeg";
|
|
143
|
+
return "application/octet-stream";
|
|
144
|
+
}
|
|
145
|
+
async function filesUnder(root, directory = root) {
|
|
146
|
+
const result = [];
|
|
147
|
+
for (const name of (await readdir(directory)).sort()) {
|
|
148
|
+
const path = resolve(directory, name);
|
|
149
|
+
const info = await stat(path);
|
|
150
|
+
if (info.isDirectory()) result.push(...await filesUnder(root, path));
|
|
151
|
+
else if (info.isFile() && name !== "forgeax-dist.json") result.push(path);
|
|
152
|
+
}
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
async function writeDistManifest(facts, base, rootInput = resolve(facts.root, "dist")) {
|
|
156
|
+
const root = resolve(rootInput);
|
|
157
|
+
const artifacts = await Promise.all(
|
|
158
|
+
(await filesUnder(root)).map(async (path) => {
|
|
159
|
+
const bytes = await readFile(path);
|
|
160
|
+
return {
|
|
161
|
+
path: relative(root, path).split(sep).join("/"),
|
|
162
|
+
mediaType: mediaType(path),
|
|
163
|
+
bytes: bytes.byteLength,
|
|
164
|
+
sha256: createHash("sha256").update(bytes).digest("hex")
|
|
165
|
+
};
|
|
166
|
+
})
|
|
167
|
+
);
|
|
168
|
+
const manifest = {
|
|
169
|
+
schemaVersion: "1.0.0",
|
|
170
|
+
project: { id: facts.id, name: facts.name },
|
|
171
|
+
base,
|
|
172
|
+
runtime: {
|
|
173
|
+
packIndexUrl: "pack-index.json",
|
|
174
|
+
shaderManifestUrl: "shaders/manifest.json"
|
|
175
|
+
},
|
|
176
|
+
artifacts
|
|
177
|
+
};
|
|
178
|
+
await writeFile(resolve(root, "forgeax-dist.json"), `${JSON.stringify(manifest, null, 2)}
|
|
179
|
+
`);
|
|
180
|
+
return manifest;
|
|
181
|
+
}
|
|
182
|
+
async function verifyDist(rootInput) {
|
|
183
|
+
const root = resolve(rootInput);
|
|
184
|
+
let manifest;
|
|
185
|
+
try {
|
|
186
|
+
manifest = JSON.parse(
|
|
187
|
+
await readFile(resolve(root, "forgeax-dist.json"), "utf8")
|
|
188
|
+
);
|
|
189
|
+
} catch (cause) {
|
|
190
|
+
return {
|
|
191
|
+
ok: false,
|
|
192
|
+
error: {
|
|
193
|
+
code: "dist-manifest-unreadable",
|
|
194
|
+
expected: "a readable dist/forgeax-dist.json",
|
|
195
|
+
hint: "Run forgeax build and do not edit its derived manifest.",
|
|
196
|
+
detail: { root, reason: cause instanceof Error ? cause.message : String(cause) }
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
if (manifest.schemaVersion !== "1.0.0" || !Array.isArray(manifest.artifacts)) {
|
|
201
|
+
return {
|
|
202
|
+
ok: false,
|
|
203
|
+
error: {
|
|
204
|
+
code: "dist-manifest-invalid",
|
|
205
|
+
expected: "forgeax-dist schema 1.0.0",
|
|
206
|
+
hint: "Rebuild the game with a compatible ForgeaX DevKit.",
|
|
207
|
+
detail: { root }
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
const declaredPaths = new Set(manifest.artifacts.map((artifact) => artifact.path));
|
|
212
|
+
if (declaredPaths.size !== manifest.artifacts.length) {
|
|
213
|
+
return {
|
|
214
|
+
ok: false,
|
|
215
|
+
error: {
|
|
216
|
+
code: "dist-artifact-duplicate",
|
|
217
|
+
expected: "every dist artifact path to be declared exactly once",
|
|
218
|
+
hint: "Rebuild the derived dist manifest.",
|
|
219
|
+
detail: { root }
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
for (const artifact of manifest.artifacts) {
|
|
224
|
+
const path = resolve(root, artifact.path);
|
|
225
|
+
if (relative(root, path).startsWith("..")) {
|
|
226
|
+
return {
|
|
227
|
+
ok: false,
|
|
228
|
+
error: {
|
|
229
|
+
code: "dist-path-escape",
|
|
230
|
+
expected: "every artifact path to remain inside dist",
|
|
231
|
+
hint: "Rebuild the derived dist manifest.",
|
|
232
|
+
detail: { path: artifact.path }
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
const bytes = await readFile(path);
|
|
238
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
239
|
+
if (bytes.byteLength !== artifact.bytes || digest !== artifact.sha256) {
|
|
240
|
+
return {
|
|
241
|
+
ok: false,
|
|
242
|
+
error: {
|
|
243
|
+
code: "dist-artifact-mismatch",
|
|
244
|
+
expected: "artifact bytes and SHA-256 to match forgeax-dist.json",
|
|
245
|
+
hint: "Restore the built artifact or rebuild the complete dist.",
|
|
246
|
+
detail: { path: artifact.path }
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
} catch {
|
|
251
|
+
return {
|
|
252
|
+
ok: false,
|
|
253
|
+
error: {
|
|
254
|
+
code: "dist-artifact-missing",
|
|
255
|
+
expected: "every declared dist artifact to exist",
|
|
256
|
+
hint: "Restore the built artifact or rebuild the complete dist.",
|
|
257
|
+
detail: { path: artifact.path }
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
for (const required of [
|
|
263
|
+
"index.html",
|
|
264
|
+
manifest.runtime.packIndexUrl,
|
|
265
|
+
manifest.runtime.shaderManifestUrl
|
|
266
|
+
]) {
|
|
267
|
+
if (!manifest.artifacts.some((artifact) => artifact.path === required)) {
|
|
268
|
+
return {
|
|
269
|
+
ok: false,
|
|
270
|
+
error: {
|
|
271
|
+
code: "dist-required-artifact-missing",
|
|
272
|
+
expected: "HTML, Pack index, and Shader manifest in the dist closure",
|
|
273
|
+
hint: "Rebuild after repairing the owning producer.",
|
|
274
|
+
detail: { path: required }
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
for (const path of await filesUnder(root)) {
|
|
280
|
+
const artifactPath = relative(root, path).split(sep).join("/");
|
|
281
|
+
if (!declaredPaths.has(artifactPath)) {
|
|
282
|
+
return {
|
|
283
|
+
ok: false,
|
|
284
|
+
error: {
|
|
285
|
+
code: "dist-artifact-undeclared",
|
|
286
|
+
expected: "every file in dist to be covered by forgeax-dist.json",
|
|
287
|
+
hint: "Remove the foreign file or rebuild the complete dist.",
|
|
288
|
+
detail: { path: artifactPath }
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return { ok: true, value: manifest };
|
|
294
|
+
}
|
|
295
|
+
var init_dist = __esm({
|
|
296
|
+
"src/dist.ts"() {
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
function ignoreDevKitCatalogPath(path) {
|
|
300
|
+
const normalized = path.replace(/\\/g, "/");
|
|
301
|
+
if (normalized.split("/").includes("shaders")) return true;
|
|
302
|
+
if (!normalized.endsWith(".meta.json")) return false;
|
|
303
|
+
try {
|
|
304
|
+
const meta = JSON.parse(readFileSync(path, "utf8"));
|
|
305
|
+
return meta.importer === "game-default-target-profile";
|
|
306
|
+
} catch {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function devKitDdcRoots(projectRoot2) {
|
|
311
|
+
return {
|
|
312
|
+
buildCacheRoot: resolve(projectRoot2, ".forgeax", "ddc", "build-cache"),
|
|
313
|
+
projectDdcRoot: resolve(projectRoot2, ".forgeax", "ddc", "v2")
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
function isResourcePreviewIgnoredPath(path) {
|
|
317
|
+
return ignoreDevKitCatalogPath(path) || path.endsWith(".wgsl.meta.json") || path.endsWith("target-profile.json.meta.json");
|
|
318
|
+
}
|
|
319
|
+
function isProjectSourceIgnoredPath(path) {
|
|
320
|
+
return ignoreDevKitCatalogPath(path) || path.endsWith(".wgsl.meta.json");
|
|
321
|
+
}
|
|
322
|
+
function findEngineWorkspaceRoot() {
|
|
323
|
+
let cursor = dirname(fileURLToPath(import.meta.url));
|
|
324
|
+
for (; ; ) {
|
|
325
|
+
if (existsSync(resolve(cursor, "pnpm-workspace.yaml"))) return cursor;
|
|
326
|
+
const parent = dirname(cursor);
|
|
327
|
+
if (parent === cursor) return void 0;
|
|
328
|
+
cursor = parent;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
async function engineWorkspacePackages() {
|
|
332
|
+
if (engineWorkspacePackagesPromise !== void 0) return engineWorkspacePackagesPromise;
|
|
333
|
+
engineWorkspacePackagesPromise = (async () => {
|
|
334
|
+
const workspaceRoot = findEngineWorkspaceRoot();
|
|
335
|
+
if (workspaceRoot === void 0) return /* @__PURE__ */ new Map();
|
|
336
|
+
const packageRoot = resolve(workspaceRoot, "packages");
|
|
337
|
+
const packages = /* @__PURE__ */ new Map();
|
|
338
|
+
for (const entry of await readdir(packageRoot, { withFileTypes: true })) {
|
|
339
|
+
if (!entry.isDirectory()) continue;
|
|
340
|
+
const root = resolve(packageRoot, entry.name);
|
|
341
|
+
try {
|
|
342
|
+
const manifest = JSON.parse(
|
|
343
|
+
await readFile(resolve(root, "package.json"), "utf8")
|
|
344
|
+
);
|
|
345
|
+
if (manifest === null || typeof manifest !== "object") continue;
|
|
346
|
+
const name = manifest.name;
|
|
347
|
+
if (typeof name === "string" && name.startsWith("@forgeax/engine-")) {
|
|
348
|
+
packages.set(name, { root, manifest });
|
|
349
|
+
}
|
|
350
|
+
} catch {
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return packages;
|
|
354
|
+
})();
|
|
355
|
+
return engineWorkspacePackagesPromise;
|
|
356
|
+
}
|
|
357
|
+
function conditionalExport(value) {
|
|
358
|
+
if (typeof value === "string") return value;
|
|
359
|
+
if (value === null || Array.isArray(value)) return void 0;
|
|
360
|
+
for (const condition of ["browser", "import", "node", "default"]) {
|
|
361
|
+
const candidate = value[condition];
|
|
362
|
+
if (candidate === void 0) continue;
|
|
363
|
+
const selected = conditionalExport(candidate);
|
|
364
|
+
if (selected !== void 0) return selected;
|
|
365
|
+
}
|
|
366
|
+
return void 0;
|
|
367
|
+
}
|
|
368
|
+
function packageExportTarget(manifest, subpath) {
|
|
369
|
+
const exportsValue = manifest.exports;
|
|
370
|
+
if (exportsValue === void 0) {
|
|
371
|
+
if (subpath.length > 0) return void 0;
|
|
372
|
+
const main = manifest.module ?? manifest.main;
|
|
373
|
+
return typeof main === "string" ? main : void 0;
|
|
374
|
+
}
|
|
375
|
+
if (typeof exportsValue === "string" || exportsValue === null) {
|
|
376
|
+
return subpath.length === 0 ? conditionalExport(exportsValue) : void 0;
|
|
377
|
+
}
|
|
378
|
+
const keys = Object.keys(exportsValue);
|
|
379
|
+
const subpathMap = keys.some((key) => key === "." || key.startsWith("./"));
|
|
380
|
+
if (!subpathMap) return subpath.length === 0 ? conditionalExport(exportsValue) : void 0;
|
|
381
|
+
const requested = subpath.length === 0 ? "." : `./${subpath}`;
|
|
382
|
+
const exact = exportsValue[requested];
|
|
383
|
+
if (exact !== void 0) return conditionalExport(exact);
|
|
384
|
+
for (const key of keys) {
|
|
385
|
+
const marker = key.indexOf("*");
|
|
386
|
+
if (marker < 0) continue;
|
|
387
|
+
const prefix = key.slice(0, marker);
|
|
388
|
+
const suffix = key.slice(marker + 1);
|
|
389
|
+
if (!requested.startsWith(prefix) || !requested.endsWith(suffix)) continue;
|
|
390
|
+
const replacement = requested.slice(prefix.length, requested.length - suffix.length);
|
|
391
|
+
const exportTarget = exportsValue[key];
|
|
392
|
+
if (exportTarget === void 0) continue;
|
|
393
|
+
const selected = conditionalExport(exportTarget);
|
|
394
|
+
return selected?.replaceAll("*", replacement);
|
|
395
|
+
}
|
|
396
|
+
return void 0;
|
|
397
|
+
}
|
|
398
|
+
function engineWorkspaceImport(source, packages) {
|
|
399
|
+
if (!source.startsWith("@forgeax/engine-")) return void 0;
|
|
400
|
+
const separator = source.indexOf("/", "@forgeax/engine-".length);
|
|
401
|
+
const packageName = separator < 0 ? source : source.slice(0, separator);
|
|
402
|
+
const subpath = separator < 0 ? "" : source.slice(separator + 1);
|
|
403
|
+
const packageInfo = packages.get(packageName);
|
|
404
|
+
if (packageInfo === void 0) return void 0;
|
|
405
|
+
const target = packageExportTarget(packageInfo.manifest, subpath);
|
|
406
|
+
if (target === void 0) return void 0;
|
|
407
|
+
const absolute = resolve(packageInfo.root, target);
|
|
408
|
+
const inside = relative(packageInfo.root, absolute);
|
|
409
|
+
if (inside === ".." || inside.startsWith(`..${sep}`) || absolute === packageInfo.root) {
|
|
410
|
+
return void 0;
|
|
411
|
+
}
|
|
412
|
+
return absolute;
|
|
413
|
+
}
|
|
414
|
+
async function createEngineWorkspaceResolver() {
|
|
415
|
+
const packages = await engineWorkspacePackages();
|
|
416
|
+
if (packages.size === 0) return void 0;
|
|
417
|
+
return {
|
|
418
|
+
name: "forgeax:devkit-engine-workspace-resolver",
|
|
419
|
+
async resolveId(source, importer) {
|
|
420
|
+
const bareSource = source.split("?", 1)[0] ?? source;
|
|
421
|
+
if (!bareSource.startsWith("@forgeax/engine-")) return null;
|
|
422
|
+
try {
|
|
423
|
+
const resolved = await this.resolve(source, importer, { skipSelf: true });
|
|
424
|
+
if (resolved !== null) return resolved;
|
|
425
|
+
} catch {
|
|
426
|
+
}
|
|
427
|
+
return engineWorkspaceImport(bareSource, packages);
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
async function resolveCanonicalKit() {
|
|
432
|
+
const packageJson = hostRequire.resolve("@forgeax/engine-preview/package.json");
|
|
433
|
+
const root = resolve(packageJson, "..", "assets/canonical-kit");
|
|
434
|
+
if (!existsSync(root)) throw new Error(`canonical preview kit root is missing: ${root}`);
|
|
435
|
+
const receiptPath = resolve(root, "cook-receipt.json");
|
|
436
|
+
const receipt = validateCanonicalKitReceipt(JSON.parse(await readFile(receiptPath, "utf8")));
|
|
437
|
+
if (!receipt.ok) {
|
|
438
|
+
throw new Error(`${receipt.error.code}: ${receipt.error.detail.field}`);
|
|
439
|
+
}
|
|
440
|
+
const sourcePath = resolve(root, receipt.value.transport.source);
|
|
441
|
+
const metaPath = resolve(root, receipt.value.transport.meta);
|
|
442
|
+
if (!existsSync(sourcePath) || !existsSync(metaPath)) {
|
|
443
|
+
throw new Error("canonical preview kit source and Meta must be package-owned files");
|
|
444
|
+
}
|
|
445
|
+
return { root, guid: receipt.value.source.guid };
|
|
446
|
+
}
|
|
447
|
+
function moduleSpecifier(from, to) {
|
|
448
|
+
const value = relative(from, to).split(sep).join("/");
|
|
449
|
+
return value.startsWith(".") ? value : `./${value}`;
|
|
450
|
+
}
|
|
451
|
+
function catalogModules(facts) {
|
|
452
|
+
const modules = /* @__PURE__ */ new Map();
|
|
453
|
+
const visit = (entries2, inheritedRealm = "engine") => {
|
|
454
|
+
for (const entry of entries2) {
|
|
455
|
+
const realm = entry.realm ?? inheritedRealm;
|
|
456
|
+
if (entry.group === true) {
|
|
457
|
+
visit(entry.config, realm);
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
if (entry.name.startsWith("cordis:") || realm === "build") continue;
|
|
461
|
+
const current = modules.get(entry.name);
|
|
462
|
+
if (current !== void 0 && current.realm !== realm) {
|
|
463
|
+
throw new Error(`plugin module ${entry.name} is assigned to multiple realms`);
|
|
464
|
+
}
|
|
465
|
+
modules.set(entry.name, { name: entry.name, realm });
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
visit(facts.plugins);
|
|
469
|
+
return [...modules.values()];
|
|
470
|
+
}
|
|
471
|
+
function catalogImport(facts, generated, name) {
|
|
472
|
+
const specifier = name.startsWith(".") ? moduleSpecifier(generated, resolve(facts.root, name)) : name;
|
|
473
|
+
return `() => import(${JSON.stringify(specifier)})`;
|
|
474
|
+
}
|
|
475
|
+
function projectImporters(facts) {
|
|
476
|
+
const specs = facts.assetImporters ?? [];
|
|
477
|
+
if (specs.length === 0) return [];
|
|
478
|
+
const createJiti = hostRequire("jiti");
|
|
479
|
+
const load = createJiti(resolve(facts.root, "package.json"), { esmResolve: true });
|
|
480
|
+
return specs.map((spec) => {
|
|
481
|
+
const separator = spec.lastIndexOf("#");
|
|
482
|
+
const moduleSpecifier2 = separator < 0 ? spec : spec.slice(0, separator);
|
|
483
|
+
const exportName = separator < 0 ? "default" : spec.slice(separator + 1);
|
|
484
|
+
const modulePath = moduleSpecifier2.startsWith(".") ? resolve(facts.root, moduleSpecifier2) : moduleSpecifier2;
|
|
485
|
+
const namespace = load(modulePath);
|
|
486
|
+
if (namespace === null || typeof namespace !== "object") {
|
|
487
|
+
throw new Error(`project-asset-importer-module-invalid: ${spec}`);
|
|
488
|
+
}
|
|
489
|
+
const factory = namespace[exportName];
|
|
490
|
+
if (typeof factory !== "function") {
|
|
491
|
+
throw new Error(`project-asset-importer-export-missing: ${spec}`);
|
|
492
|
+
}
|
|
493
|
+
const importer = factory();
|
|
494
|
+
if (importer === null || typeof importer !== "object") {
|
|
495
|
+
throw new Error(`project-asset-importer-invalid: ${spec}`);
|
|
496
|
+
}
|
|
497
|
+
return importer;
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
function hostSource(facts, bootstrapRoot, canonicalEnvironmentGuid) {
|
|
501
|
+
const generated = resolve(facts.root, ".forgeax", "generated");
|
|
502
|
+
const plugins = bootstrapRoot === "resource-bootstrap" ? [] : [
|
|
503
|
+
`webAudioPlugin()`,
|
|
504
|
+
`audioPlugin()`,
|
|
505
|
+
...facts.physics === void 0 ? [] : [`physicsPlugin('${facts.physics === "2d" ? "rapier-2d" : "rapier-3d"}')`]
|
|
506
|
+
];
|
|
507
|
+
const bootstrapPlugin = facts.bootstrapEntry === void 0 ? "" : `const bootstrapModule = await import(${JSON.stringify(
|
|
508
|
+
moduleSpecifier(generated, resolve(facts.root, facts.bootstrapEntry))
|
|
509
|
+
)});
|
|
510
|
+
if (typeof bootstrapModule.bootstrap !== 'function') {
|
|
511
|
+
throw new Error('forgeax: project entry must export bootstrap(world, context)');
|
|
512
|
+
}
|
|
513
|
+
await app.pluginContext.plugin({
|
|
514
|
+
name: 'project-entry-bootstrap',
|
|
515
|
+
inject: ['gameHost'],
|
|
516
|
+
async apply(ctx) {
|
|
517
|
+
if (ctx.gameHost === undefined) throw new Error('forgeax: gameHost unavailable');
|
|
518
|
+
await bootstrapModule.bootstrap(ctx.gameHost.app.world, ctx.gameHost);
|
|
519
|
+
},
|
|
520
|
+
});`;
|
|
521
|
+
return `import { forgeaxBundlerAdapter } from 'virtual:forgeax/bundler';
|
|
522
|
+
import { createApp, createToolPreviewHost, createToolPreviewRecipe, fitToolPreviewCameraToAabb, gameHostPlugin, replayToolPreviewCapture } from '@forgeax/engine-app';
|
|
523
|
+
import { createCatalogSource } from '@forgeax/engine-assets-runtime';
|
|
524
|
+
import { installCatalogLoader, projectPluginEntries } from '@forgeax/engine-plugin/loader';
|
|
525
|
+
import { audioPlugin } from '@forgeax/engine-audio';
|
|
526
|
+
import { webAudioPlugin } from '@forgeax/engine-audio-webaudio';
|
|
527
|
+
import { physicsPlugin } from '@forgeax/engine-physics';
|
|
528
|
+
import { createPrimitiveMesh } from '@forgeax/engine-geometry';
|
|
529
|
+
import { createDevImportTransport } from '@forgeax/engine-runtime';
|
|
530
|
+
import { mat4 } from '@forgeax/engine-math';
|
|
531
|
+
import { CAMERA_PROJECTION_ORTHOGRAPHIC, Camera, DirectionalLight, Materials, MeshFilter, MeshRenderer, Skylight, SkyboxBackground, TONEMAP_NONE, TONEMAP_REINHARD_EXTENDED } from '@forgeax/engine-render';
|
|
532
|
+
import { projectSceneAsset, Transform, worldInstantiateScene } from '@forgeax/engine-scene';
|
|
533
|
+
import { createStandaloneRuntimeAssetBinding } from '@forgeax/engine-types';
|
|
534
|
+
import { ParticleEffectPlayer, vfxGpuEffectContribution } from '@forgeax/engine-vfx';
|
|
535
|
+
import { createVfxRuntimeHost } from '@forgeax/engine-vfx-render';
|
|
536
|
+
|
|
537
|
+
const pluginCatalog = new Map([
|
|
538
|
+
${catalogModules(facts).map(
|
|
539
|
+
({ name, realm }) => ` [${JSON.stringify(name)}, { realm: ${JSON.stringify(realm)}, load: ${catalogImport(facts, generated, name)} }],`
|
|
540
|
+
).join("\n")}
|
|
541
|
+
]);
|
|
542
|
+
const pluginEntries = ${JSON.stringify(facts.plugins, null, 2)};
|
|
543
|
+
const bootstrapRoot = ${JSON.stringify(bootstrapRoot)};
|
|
544
|
+
const canonicalEnvironmentGuid = ${JSON.stringify(canonicalEnvironmentGuid ?? null)};
|
|
545
|
+
const resourceValue = new URLSearchParams(location.search).get('forgeax-resource-preview');
|
|
546
|
+
const resource = resourceValue === null ? undefined : JSON.parse(resourceValue);
|
|
547
|
+
const previewCameraEntities = new WeakMap();
|
|
548
|
+
const previewCameraTargets = new WeakMap();
|
|
549
|
+
let previewWorld;
|
|
550
|
+
const vfxRuntimeHost = resource?.kind === 'vfx'
|
|
551
|
+
? createVfxRuntimeHost({
|
|
552
|
+
camera: {
|
|
553
|
+
read(world) {
|
|
554
|
+
const cameraEntity = previewCameraEntities.get(world);
|
|
555
|
+
if (cameraEntity === undefined) return undefined;
|
|
556
|
+
const transform = world.get(cameraEntity, Transform);
|
|
557
|
+
const camera = world.get(cameraEntity, Camera);
|
|
558
|
+
if (!transform.ok || !camera.ok) return undefined;
|
|
559
|
+
const position = new Float32Array(transform.value.pos);
|
|
560
|
+
const target = previewCameraTargets.get(world) ?? [0, 0, 0];
|
|
561
|
+
return {
|
|
562
|
+
position,
|
|
563
|
+
right: new Float32Array([1, 0, 0]),
|
|
564
|
+
up: new Float32Array([0, 1, 0]),
|
|
565
|
+
viewProjection: mat4.computeViewProj(
|
|
566
|
+
mat4.create(),
|
|
567
|
+
position,
|
|
568
|
+
target,
|
|
569
|
+
[0, 1, 0],
|
|
570
|
+
camera.value.fov,
|
|
571
|
+
camera.value.aspect,
|
|
572
|
+
camera.value.near,
|
|
573
|
+
camera.value.far,
|
|
574
|
+
),
|
|
575
|
+
};
|
|
576
|
+
},
|
|
577
|
+
},
|
|
578
|
+
})
|
|
579
|
+
: undefined;
|
|
580
|
+
|
|
581
|
+
function previewVfxBounds(payload) {
|
|
582
|
+
if (payload?.kind !== 'particle-effect' || payload.program?.emitters?.length === 0) return undefined;
|
|
583
|
+
let minX = Infinity;
|
|
584
|
+
let minY = Infinity;
|
|
585
|
+
let minZ = Infinity;
|
|
586
|
+
let maxX = -Infinity;
|
|
587
|
+
let maxY = -Infinity;
|
|
588
|
+
let maxZ = -Infinity;
|
|
589
|
+
for (const emitter of payload.program.emitters) {
|
|
590
|
+
const bounds = emitter.bounds;
|
|
591
|
+
if (bounds?.kind === 'sphere' && Array.isArray(bounds.center) && typeof bounds.radius === 'number') {
|
|
592
|
+
const [x, y, z] = bounds.center;
|
|
593
|
+
if (![x, y, z, bounds.radius].every((value) => typeof value === 'number' && Number.isFinite(value)) || bounds.radius < 0) return undefined;
|
|
594
|
+
minX = Math.min(minX, x - bounds.radius);
|
|
595
|
+
minY = Math.min(minY, y - bounds.radius);
|
|
596
|
+
minZ = Math.min(minZ, z - bounds.radius);
|
|
597
|
+
maxX = Math.max(maxX, x + bounds.radius);
|
|
598
|
+
maxY = Math.max(maxY, y + bounds.radius);
|
|
599
|
+
maxZ = Math.max(maxZ, z + bounds.radius);
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
if (bounds?.kind === 'aabb' && Array.isArray(bounds.min) && Array.isArray(bounds.max)) {
|
|
603
|
+
const [loX, loY, loZ] = bounds.min;
|
|
604
|
+
const [hiX, hiY, hiZ] = bounds.max;
|
|
605
|
+
if (![loX, loY, loZ, hiX, hiY, hiZ].every((value) => typeof value === 'number' && Number.isFinite(value))) return undefined;
|
|
606
|
+
minX = Math.min(minX, loX);
|
|
607
|
+
minY = Math.min(minY, loY);
|
|
608
|
+
minZ = Math.min(minZ, loZ);
|
|
609
|
+
maxX = Math.max(maxX, hiX);
|
|
610
|
+
maxY = Math.max(maxY, hiY);
|
|
611
|
+
maxZ = Math.max(maxZ, hiZ);
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
return undefined;
|
|
615
|
+
}
|
|
616
|
+
if (![minX, minY, minZ, maxX, maxY, maxZ].every(Number.isFinite)) return undefined;
|
|
617
|
+
const center = [(minX + maxX) / 2, (minY + maxY) / 2, (minZ + maxZ) / 2];
|
|
618
|
+
const radius = Math.max(1, Math.hypot(maxX - minX, maxY - minY, maxZ - minZ));
|
|
619
|
+
return { aabb: [minX, minY, minZ, maxX, maxY, maxZ], center, radius };
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const canvas = document.querySelector('#app');
|
|
623
|
+
if (!(canvas instanceof HTMLCanvasElement)) throw new Error('forgeax: missing canvas');
|
|
624
|
+
const resizeCanvas = () => {
|
|
625
|
+
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
626
|
+
const width = Math.max(1, Math.round(canvas.clientWidth * dpr));
|
|
627
|
+
const height = Math.max(1, Math.round(canvas.clientHeight * dpr));
|
|
628
|
+
if (canvas.width !== width) canvas.width = width;
|
|
629
|
+
if (canvas.height !== height) canvas.height = height;
|
|
630
|
+
};
|
|
631
|
+
const resizeObserver = new ResizeObserver(resizeCanvas);
|
|
632
|
+
resizeObserver.observe(canvas);
|
|
633
|
+
resizeCanvas();
|
|
634
|
+
const binding = import.meta.env.DEV
|
|
635
|
+
? createStandaloneRuntimeAssetBinding(${JSON.stringify(facts.id)})
|
|
636
|
+
: undefined;
|
|
637
|
+
const assetCatalog = binding === undefined
|
|
638
|
+
? undefined
|
|
639
|
+
: createCatalogSource({ url: binding.catalogUrl, expectedScope: binding });
|
|
640
|
+
const bundler = {
|
|
641
|
+
...forgeaxBundlerAdapter(),
|
|
642
|
+
...(binding === undefined ? {} : { importTransport: createDevImportTransport(binding) }),
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
function previewStable(value) {
|
|
646
|
+
if (value instanceof ArrayBuffer) return 'ArrayBuffer:' + JSON.stringify(Array.from(new Uint8Array(value)));
|
|
647
|
+
if (ArrayBuffer.isView(value)) return value.constructor.name + ':' + JSON.stringify(Array.from(new Uint8Array(value.buffer, value.byteOffset, value.byteLength)));
|
|
648
|
+
if (Array.isArray(value)) return '[' + value.map(previewStable).join(',') + ']';
|
|
649
|
+
if (value !== null && typeof value === 'object') {
|
|
650
|
+
const record = value;
|
|
651
|
+
return '{' + Object.keys(record).sort().map((key) => JSON.stringify(key) + ':' + previewStable(record[key])).join(',') + '}';
|
|
652
|
+
}
|
|
653
|
+
return JSON.stringify(value) ?? 'null';
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
async function previewDigest(value) {
|
|
657
|
+
const bytes = new TextEncoder().encode(previewStable(value));
|
|
658
|
+
const digest = await crypto.subtle.digest('SHA-256', bytes);
|
|
659
|
+
return 'sha256:' + Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
async function previewOwnerFacts(assets, resource, payload) {
|
|
663
|
+
if (resource.kind === 'material') {
|
|
664
|
+
const primaryPass = Array.isArray(payload.passes) ? payload.passes[0] : undefined;
|
|
665
|
+
const program = primaryPass?.program?.module;
|
|
666
|
+
const pass = primaryPass?.name;
|
|
667
|
+
return {
|
|
668
|
+
subjectDigest: await previewDigest(payload),
|
|
669
|
+
bindingsDigest: await previewDigest(payload.passes),
|
|
670
|
+
closureDigest: await previewDigest(payload.values ?? payload.passes),
|
|
671
|
+
...(typeof program === 'string' ? { program } : {}),
|
|
672
|
+
...(typeof pass === 'string' ? { pass } : {}),
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
if (resource.kind === 'mesh') {
|
|
676
|
+
return {
|
|
677
|
+
subjectDigest: await previewDigest(payload),
|
|
678
|
+
vertexDigest: await previewDigest(payload.vertices),
|
|
679
|
+
indexDigest: await previewDigest(payload.indices ?? []),
|
|
680
|
+
submeshDigest: await previewDigest(payload.submeshes),
|
|
681
|
+
aabbDigest: await previewDigest(payload.aabb),
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
if (resource.kind === 'texture') {
|
|
685
|
+
const mipCount = typeof payload.mipLevelCount === 'number' ? payload.mipLevelCount : 1;
|
|
686
|
+
const filter = 'linear';
|
|
687
|
+
return {
|
|
688
|
+
subjectDigest: await previewDigest(payload),
|
|
689
|
+
boundDigest: await previewDigest({ width: payload.width, height: payload.height, format: payload.format }),
|
|
690
|
+
uvDigest: await previewDigest({ offset: [0, 0], scale: [1, 1], rotation: 0 }),
|
|
691
|
+
bindingDigest: await previewDigest({ format: payload.format, colorSpace: payload.colorSpace, filter, mipCount }),
|
|
692
|
+
format: payload.format,
|
|
693
|
+
colorSpace: payload.colorSpace,
|
|
694
|
+
filter,
|
|
695
|
+
mipCount,
|
|
696
|
+
dimensions: [payload.width, payload.height],
|
|
697
|
+
payloadClass: 'color',
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
return undefined;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
async function prepareProject(app) {
|
|
704
|
+
previewWorld = app.world;
|
|
705
|
+
const assets = app.assets;
|
|
706
|
+
let canonicalEnvironment;
|
|
707
|
+
if (resource !== undefined) {
|
|
708
|
+
if (resource.kind !== 'texture') {
|
|
709
|
+
if (canonicalEnvironmentGuid === null) throw new Error('canonical preview environment is unavailable');
|
|
710
|
+
const environment = await assets.load(canonicalEnvironmentGuid, 'equirect');
|
|
711
|
+
if (!environment.ok) throw environment.error;
|
|
712
|
+
if (environment.value.kind !== 'equirect') {
|
|
713
|
+
throw new Error(\`canonical preview environment expected equirect, received \${environment.value.kind}\`);
|
|
714
|
+
}
|
|
715
|
+
if (environment.value.width <= 0 || environment.value.height <= 0 || environment.value.data.byteLength === 0) {
|
|
716
|
+
throw new Error('canonical preview environment must provide decoded equirect pixels');
|
|
717
|
+
}
|
|
718
|
+
canonicalEnvironment = app.world.allocSharedRef('EquirectAsset', environment.value);
|
|
719
|
+
}
|
|
720
|
+
if (resource.kind === 'vfx') {
|
|
721
|
+
if (vfxRuntimeHost === undefined) throw new Error('VFX preview host was not created');
|
|
722
|
+
const attached = await vfxRuntimeHost.attachWorld({ world: app.world, assets });
|
|
723
|
+
if (!attached.ok) throw attached.error;
|
|
724
|
+
}
|
|
725
|
+
const loaded = await assets.load(resource.guid, resource.kind === 'vfx' ? 'particle-effect' : resource.kind);
|
|
726
|
+
if (!loaded.ok) throw loaded.error;
|
|
727
|
+
const payload = loaded.value;
|
|
728
|
+
if (resource.kind === 'vfx' && payload.kind !== 'particle-effect') {
|
|
729
|
+
throw new Error(\`VFX preview expected ParticleEffectAsset, received \${payload.kind}\`);
|
|
730
|
+
}
|
|
731
|
+
const vfxBounds = resource.kind === 'vfx' ? previewVfxBounds(payload) : undefined;
|
|
732
|
+
if (resource.kind === 'vfx' && vfxBounds === undefined) {
|
|
733
|
+
throw new Error('VFX preview owner did not publish finite emitter bounds');
|
|
734
|
+
}
|
|
735
|
+
const meshMaterialHandles = resource.kind === 'mesh' && payload.kind === 'mesh'
|
|
736
|
+
? await Promise.all(payload.materialSlots.map(async (slot) => {
|
|
737
|
+
if (slot.defaultMaterial === undefined) return 0;
|
|
738
|
+
const material = await assets.load(slot.defaultMaterial, 'material');
|
|
739
|
+
if (!material.ok) throw material.error;
|
|
740
|
+
if (material.value.kind !== 'material') {
|
|
741
|
+
throw new Error('mesh material slot resolved to a non-material asset');
|
|
742
|
+
}
|
|
743
|
+
return app.world.allocSharedRef('MaterialAsset', material.value);
|
|
744
|
+
}))
|
|
745
|
+
: undefined;
|
|
746
|
+
const meshHandle = resource.kind === 'mesh' && payload.kind === 'mesh'
|
|
747
|
+
? app.world.allocSharedRef('MeshAsset', payload)
|
|
748
|
+
: app.world.internSharedRef(
|
|
749
|
+
'MeshAsset',
|
|
750
|
+
createPrimitiveMesh(resource.kind === 'texture' ? 'quad' : 'sphere').unwrap(),
|
|
751
|
+
);
|
|
752
|
+
const textureHandle = resource.kind === 'texture' && payload.kind === 'texture'
|
|
753
|
+
? app.world.allocSharedRef('TextureAsset', payload)
|
|
754
|
+
: undefined;
|
|
755
|
+
if (textureHandle !== undefined && payload.kind === 'texture') {
|
|
756
|
+
const bytes = payload.data instanceof Uint8ClampedArray
|
|
757
|
+
? new Uint8Array(payload.data.buffer, payload.data.byteOffset, payload.data.byteLength)
|
|
758
|
+
: payload.data;
|
|
759
|
+
const uploaded = await app.renderer.store.uploadTexture(textureHandle, payload, {
|
|
760
|
+
bytes,
|
|
761
|
+
width: payload.width,
|
|
762
|
+
height: payload.height,
|
|
763
|
+
mime: 'image/png',
|
|
764
|
+
colorSpace: payload.colorSpace,
|
|
765
|
+
mipmap: payload.mipmap,
|
|
766
|
+
});
|
|
767
|
+
if (!uploaded.ok) throw uploaded.error;
|
|
768
|
+
}
|
|
769
|
+
const materialPayload = resource.kind === 'texture' && payload.kind === 'texture'
|
|
770
|
+
? Materials.unlit([1, 1, 1, 1], { baseColorTexture: textureHandle })
|
|
771
|
+
: resource.kind === 'material' && payload.kind === 'material'
|
|
772
|
+
? payload
|
|
773
|
+
: undefined;
|
|
774
|
+
const materialHandle = materialPayload === undefined
|
|
775
|
+
? undefined
|
|
776
|
+
: app.world.allocSharedRef('MaterialAsset', materialPayload);
|
|
777
|
+
const materialBindings = resource.kind === 'mesh'
|
|
778
|
+
? meshMaterialHandles
|
|
779
|
+
: materialHandle === undefined
|
|
780
|
+
? undefined
|
|
781
|
+
: [materialHandle];
|
|
782
|
+
const rawAabb = payload.kind === 'mesh' ? payload.aabb : undefined;
|
|
783
|
+
const aabb = rawAabb !== undefined && (Array.isArray(rawAabb) || ArrayBuffer.isView(rawAabb)) && rawAabb.length === 6
|
|
784
|
+
? Array.from(rawAabb)
|
|
785
|
+
: resource.kind === 'mesh' && payload.kind === 'mesh'
|
|
786
|
+
? (() => { throw new Error('mesh owner did not publish a finite AABB'); })()
|
|
787
|
+
: [-1, -1, -1, 1, 1, 1];
|
|
788
|
+
const meshFrame = resource.kind === 'mesh'
|
|
789
|
+
? fitToolPreviewCameraToAabb(aabb, { aspect: 1, fov: Math.PI / 4 })
|
|
790
|
+
: undefined;
|
|
791
|
+
const textureWidth = resource.kind === 'texture' && payload.kind === 'texture' && typeof payload.width === 'number'
|
|
792
|
+
? Math.max(1, payload.width)
|
|
793
|
+
: 1;
|
|
794
|
+
const textureHeight = resource.kind === 'texture' && payload.kind === 'texture' && typeof payload.height === 'number'
|
|
795
|
+
? Math.max(1, payload.height)
|
|
796
|
+
: 1;
|
|
797
|
+
const textureAspect = textureWidth / textureHeight;
|
|
798
|
+
const textureScale = textureAspect >= 1
|
|
799
|
+
? [textureAspect, 1, 1]
|
|
800
|
+
: [1, 1 / textureAspect, 1];
|
|
801
|
+
const center = meshFrame?.center ?? vfxBounds?.center ?? [(aabb[0] + aabb[3]) / 2, (aabb[1] + aabb[4]) / 2, (aabb[2] + aabb[5]) / 2];
|
|
802
|
+
const radius = meshFrame?.radius ?? vfxBounds?.radius ?? Math.max(1, Math.hypot(aabb[3] - aabb[0], aabb[4] - aabb[1], aabb[5] - aabb[2]));
|
|
803
|
+
const texturePreview = resource.kind === 'texture';
|
|
804
|
+
if (resource.kind === 'vfx') {
|
|
805
|
+
const effect = app.world.allocSharedRef('ParticleEffectAsset', payload);
|
|
806
|
+
app.world.spawn(
|
|
807
|
+
{ component: Transform, data: { pos: center } },
|
|
808
|
+
{ component: ParticleEffectPlayer, data: { effect, playing: true, seed: 0, timeScale: 1 } },
|
|
809
|
+
);
|
|
810
|
+
} else {
|
|
811
|
+
app.world.spawn(
|
|
812
|
+
{ component: Transform, data: { pos: center, ...(texturePreview ? { scale: textureScale } : {}) } },
|
|
813
|
+
{ component: MeshFilter, data: { assetHandle: meshHandle } },
|
|
814
|
+
{ component: MeshRenderer, data: materialBindings === undefined ? {} : { materials: materialBindings } },
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
const cameraData = texturePreview
|
|
818
|
+
? {
|
|
819
|
+
fov: 0,
|
|
820
|
+
aspect: 1,
|
|
821
|
+
near: 0.01,
|
|
822
|
+
far: 100,
|
|
823
|
+
projection: CAMERA_PROJECTION_ORTHOGRAPHIC,
|
|
824
|
+
left: -textureScale[0] * 0.6,
|
|
825
|
+
right: textureScale[0] * 0.6,
|
|
826
|
+
bottom: -textureScale[1] * 0.6,
|
|
827
|
+
top: textureScale[1] * 0.6,
|
|
828
|
+
tonemap: TONEMAP_NONE,
|
|
829
|
+
antialias: 0,
|
|
830
|
+
bloom: 0,
|
|
831
|
+
clearColor: [0, 0, 0, 1],
|
|
832
|
+
}
|
|
833
|
+
: {
|
|
834
|
+
fov: Math.PI / 4,
|
|
835
|
+
aspect: 1,
|
|
836
|
+
near: meshFrame?.near ?? 0.01,
|
|
837
|
+
far: meshFrame?.far ?? radius * 8,
|
|
838
|
+
tonemap: TONEMAP_REINHARD_EXTENDED,
|
|
839
|
+
};
|
|
840
|
+
const cameraPosition = texturePreview
|
|
841
|
+
? [0, 0, 5]
|
|
842
|
+
: [center[0], center[1], center[2] + (meshFrame?.distance ?? radius * 2.5)];
|
|
843
|
+
const cameraEntity = app.world.spawn({ component: Camera, data: cameraData }, { component: Transform, data: { pos: cameraPosition } }).unwrap();
|
|
844
|
+
previewCameraEntities.set(app.world, cameraEntity);
|
|
845
|
+
previewCameraTargets.set(app.world, center);
|
|
846
|
+
if (!texturePreview) {
|
|
847
|
+
app.world.spawn({ component: DirectionalLight, data: { direction: [-0.5, -1, -0.3], intensity: 2 } });
|
|
848
|
+
if (canonicalEnvironment === undefined) throw new Error('canonical preview environment was not loaded');
|
|
849
|
+
app.world.spawn({ component: Skylight, data: { equirect: canonicalEnvironment } });
|
|
850
|
+
app.world.spawn({ component: SkyboxBackground, data: { equirect: canonicalEnvironment } });
|
|
851
|
+
}
|
|
852
|
+
const ownerFacts = await previewOwnerFacts(assets, resource, payload);
|
|
853
|
+
const publishedAsset = resource.kind === 'mesh' && payload.kind === 'mesh' && payload.aabb !== undefined
|
|
854
|
+
? { ...payload, aabb: Array.from(payload.aabb) }
|
|
855
|
+
: payload;
|
|
856
|
+
return {
|
|
857
|
+
kind: resource.kind,
|
|
858
|
+
guid: resource.guid,
|
|
859
|
+
asset: publishedAsset,
|
|
860
|
+
...(ownerFacts === undefined ? {} : { ownerFacts }),
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
if (bootstrapRoot === 'resource-bootstrap') return;
|
|
864
|
+
let defaultScene;
|
|
865
|
+
let defaultSceneRoot;
|
|
866
|
+
const defaultSceneGuid = ${JSON.stringify(facts.defaultScene)};
|
|
867
|
+
if (defaultSceneGuid !== undefined) {
|
|
868
|
+
const loaded = await assets.load(defaultSceneGuid, 'scene');
|
|
869
|
+
if (!loaded.ok) throw loaded.error;
|
|
870
|
+
const projected = await projectSceneAsset(app.world, loaded.value, (guid, kind) => assets.load(guid, kind));
|
|
871
|
+
if (!projected.ok) throw projected.error;
|
|
872
|
+
defaultScene = projected.value;
|
|
873
|
+
const handle = app.world.allocSharedRef('SceneAsset', projected.value);
|
|
874
|
+
const instantiated = worldInstantiateScene(app.world, handle);
|
|
875
|
+
if (!instantiated.ok) throw instantiated.error;
|
|
876
|
+
defaultSceneRoot = instantiated.value.root;
|
|
877
|
+
}
|
|
878
|
+
const uiRoot = document.querySelector('#game-ui');
|
|
879
|
+
await app.pluginContext.plugin(gameHostPlugin({
|
|
880
|
+
app,
|
|
881
|
+
assets,
|
|
882
|
+
canvas,
|
|
883
|
+
renderer: app.renderer,
|
|
884
|
+
...(defaultScene === undefined ? {} : { defaultScene }),
|
|
885
|
+
...(defaultSceneRoot === undefined ? {} : { defaultSceneRoot }),
|
|
886
|
+
uiRoot: uiRoot instanceof HTMLElement ? uiRoot : document.body,
|
|
887
|
+
setPointerLockAllowed: (allowed) => app.input?.setPointerLockAllowed?.(allowed),
|
|
888
|
+
}));
|
|
889
|
+
const { loader: pluginLoader } = await installCatalogLoader(app.pluginContext, pluginCatalog, 'engine');
|
|
890
|
+
await pluginLoader.root.update(projectPluginEntries(pluginEntries, 'engine'));
|
|
891
|
+
await pluginLoader.await();
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const query = new URLSearchParams(location.search);
|
|
895
|
+
const recipeValue = query.get('forgeax-tool-recipe');
|
|
896
|
+
const snapshotValue = query.get('forgeax-tool-snapshot');
|
|
897
|
+
const runIdValue = query.get('forgeax-tool-run-id');
|
|
898
|
+
if (query.has('forgeax-tool-replay')) {
|
|
899
|
+
globalThis.__forgeaxToolReplayHost = {
|
|
900
|
+
ready: true,
|
|
901
|
+
async run(capture) {
|
|
902
|
+
const result = await replayToolPreviewCapture(capture);
|
|
903
|
+
return result.ok ? { ok: true, result: result.value } : { ok: false, error: result.error };
|
|
904
|
+
},
|
|
905
|
+
};
|
|
906
|
+
} else if (recipeValue !== null) {
|
|
907
|
+
const previewRecipe = createToolPreviewRecipe(JSON.parse(recipeValue));
|
|
908
|
+
const host = await createToolPreviewHost({
|
|
909
|
+
...(runIdValue === null ? {} : { runId: runIdValue }),
|
|
910
|
+
recipe: previewRecipe,
|
|
911
|
+
snapshot: JSON.parse(snapshotValue ?? 'null'),
|
|
912
|
+
...(resource === undefined ? {} : { resource }),
|
|
913
|
+
canvas,
|
|
914
|
+
app: {
|
|
915
|
+
...(assetCatalog === undefined ? {} : { assetCatalog }),
|
|
916
|
+
...(vfxRuntimeHost === undefined ? {} : { assetDecoders: [vfxGpuEffectContribution] }),
|
|
917
|
+
plugins: [${plugins.join(", ")}],
|
|
918
|
+
...(vfxRuntimeHost === undefined ? {} : { features: [vfxRuntimeHost.feature] }),
|
|
919
|
+
},
|
|
920
|
+
bundler,
|
|
921
|
+
prepare: prepareProject,
|
|
922
|
+
collectResourceFacts(app, current) {
|
|
923
|
+
if (current === undefined) return current;
|
|
924
|
+
if (resource?.kind !== 'vfx') {
|
|
925
|
+
if (resource?.kind === 'material' || resource?.kind === 'mesh' || resource?.kind === 'texture') {
|
|
926
|
+
if (app.renderer.drawCalls <= 0) return current;
|
|
927
|
+
if (resource.kind === 'mesh') {
|
|
928
|
+
const asset = current.asset;
|
|
929
|
+
const aabb = asset?.aabb;
|
|
930
|
+
const submeshes = asset?.submeshes;
|
|
931
|
+
const materialSlots = asset?.materialSlots;
|
|
932
|
+
if (!Array.isArray(aabb) || aabb.length !== 6 || !Array.isArray(submeshes) || !Array.isArray(materialSlots)) {
|
|
933
|
+
return current;
|
|
934
|
+
}
|
|
935
|
+
return {
|
|
936
|
+
...current,
|
|
937
|
+
observation: {
|
|
938
|
+
...current.ownerFacts,
|
|
939
|
+
aabb,
|
|
940
|
+
submeshCount: submeshes.length,
|
|
941
|
+
materialSlotCount: materialSlots.length,
|
|
942
|
+
},
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
return {
|
|
946
|
+
...current,
|
|
947
|
+
observation: current.ownerFacts,
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
return current;
|
|
951
|
+
}
|
|
952
|
+
if (vfxRuntimeHost === undefined) return current;
|
|
953
|
+
const hostInspection = vfxRuntimeHost.inspect(app.world);
|
|
954
|
+
const player = hostInspection?.players[0];
|
|
955
|
+
const renderObservation = vfxRuntimeHost.feature.inspect();
|
|
956
|
+
if (player === undefined || renderObservation.frameNumber < 0) {
|
|
957
|
+
return current;
|
|
958
|
+
}
|
|
959
|
+
const bounds = previewVfxBounds(current.asset);
|
|
960
|
+
if (bounds === undefined) return current;
|
|
961
|
+
const emitterDigest = JSON.stringify(player.emitters.map(({ id, module, capacity }) => ({ id, module, capacity })));
|
|
962
|
+
const sampleDigest = JSON.stringify(player.emitters.map(({ id, schedule }) => ({ id, schedule })));
|
|
963
|
+
const boundsDigest = JSON.stringify(player.emitters.map(({ id, bounds: emitterBounds }) => ({ id, bounds: emitterBounds })));
|
|
964
|
+
const computeDigest = player.programFingerprint;
|
|
965
|
+
const indirectDigest = JSON.stringify(player.emitters.map(({ id, renderers }) => ({ id, renderers: renderers.map(({ kind, enabled }) => ({ kind, enabled })) })));
|
|
966
|
+
return {
|
|
967
|
+
...current,
|
|
968
|
+
observation: {
|
|
969
|
+
subjectDigest: current.asset.programFingerprint,
|
|
970
|
+
programFingerprint: player.programFingerprint,
|
|
971
|
+
emitterDigest,
|
|
972
|
+
sampleDigest,
|
|
973
|
+
boundsDigest,
|
|
974
|
+
computeDigest,
|
|
975
|
+
indirectDigest,
|
|
976
|
+
authoredBounds: bounds.aabb,
|
|
977
|
+
seed: player.seed,
|
|
978
|
+
fixedDelta: player.fixedDelta,
|
|
979
|
+
timelineFrames: previewRecipe.frames,
|
|
980
|
+
dispatches: renderObservation.dispatches,
|
|
981
|
+
indirectDraws: renderObservation.indirectDraws,
|
|
982
|
+
subjectOutputs: renderObservation.subjectOutputs,
|
|
983
|
+
},
|
|
984
|
+
};
|
|
985
|
+
},
|
|
986
|
+
onDispose: async () => {
|
|
987
|
+
if (vfxRuntimeHost === undefined || previewWorld === undefined) return;
|
|
988
|
+
const detached = await vfxRuntimeHost.detachWorld({ world: previewWorld });
|
|
989
|
+
if (!detached.ok) throw detached.error;
|
|
990
|
+
},
|
|
991
|
+
executeAction(action) {
|
|
992
|
+
return !globalThis.dispatchEvent(new CustomEvent('forgeax-tool-action', {
|
|
993
|
+
detail: action,
|
|
994
|
+
cancelable: true,
|
|
995
|
+
}));
|
|
996
|
+
},
|
|
997
|
+
});
|
|
998
|
+
if (!host.ok) {
|
|
999
|
+
globalThis.__forgeaxToolHost = {
|
|
1000
|
+
ready: true,
|
|
1001
|
+
capture: async () => ({ ok: false, error: host.error }),
|
|
1002
|
+
run: async () => ({ ok: false, error: host.error }),
|
|
1003
|
+
dispose: async () => undefined,
|
|
1004
|
+
};
|
|
1005
|
+
} else {
|
|
1006
|
+
globalThis.__forgeaxToolHost = {
|
|
1007
|
+
ready: true,
|
|
1008
|
+
async capture() {
|
|
1009
|
+
const result = await host.value.capture();
|
|
1010
|
+
return result.ok ? { ok: true, result: result.value } : { ok: false, error: result.error };
|
|
1011
|
+
},
|
|
1012
|
+
async run() {
|
|
1013
|
+
const result = await host.value.run();
|
|
1014
|
+
return result.ok ? { ok: true, result: result.value } : { ok: false, error: result.error };
|
|
1015
|
+
},
|
|
1016
|
+
dispose: () => host.value.dispose(),
|
|
1017
|
+
};
|
|
1018
|
+
window.addEventListener('pagehide', () => void host.value.dispose(), { once: true });
|
|
1019
|
+
}
|
|
1020
|
+
} else {
|
|
1021
|
+
const result = await createApp(
|
|
1022
|
+
canvas,
|
|
1023
|
+
{
|
|
1024
|
+
...(assetCatalog === undefined ? {} : { assetCatalog }),
|
|
1025
|
+
...(vfxRuntimeHost === undefined ? {} : { assetDecoders: [vfxGpuEffectContribution] }),
|
|
1026
|
+
plugins: [${plugins.join(", ")}],
|
|
1027
|
+
},
|
|
1028
|
+
bundler,
|
|
1029
|
+
);
|
|
1030
|
+
if (!result.ok) throw result.error;
|
|
1031
|
+
const app = result.value;
|
|
1032
|
+
await prepareProject(app);
|
|
1033
|
+
${bootstrapPlugin}
|
|
1034
|
+
app.start().unwrap();
|
|
1035
|
+
let disposed = false;
|
|
1036
|
+
const dispose = () => {
|
|
1037
|
+
if (disposed) return;
|
|
1038
|
+
disposed = true;
|
|
1039
|
+
resizeObserver.disconnect();
|
|
1040
|
+
void app.dispose();
|
|
1041
|
+
};
|
|
1042
|
+
window.addEventListener('pagehide', dispose);
|
|
1043
|
+
}
|
|
1044
|
+
`;
|
|
1045
|
+
}
|
|
1046
|
+
function escapeHtml(value) {
|
|
1047
|
+
return value.replace(/[&<>"']/g, (character) => {
|
|
1048
|
+
switch (character) {
|
|
1049
|
+
case "&":
|
|
1050
|
+
return "&";
|
|
1051
|
+
case "<":
|
|
1052
|
+
return "<";
|
|
1053
|
+
case ">":
|
|
1054
|
+
return ">";
|
|
1055
|
+
case '"':
|
|
1056
|
+
return """;
|
|
1057
|
+
case "'":
|
|
1058
|
+
return "'";
|
|
1059
|
+
}
|
|
1060
|
+
return character;
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
function htmlSource(title) {
|
|
1064
|
+
return `<!doctype html>
|
|
1065
|
+
<html lang="en">
|
|
1066
|
+
<head>
|
|
1067
|
+
<meta charset="UTF-8" />
|
|
1068
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
1069
|
+
<link rel="icon" href="data:," />
|
|
1070
|
+
<title>${escapeHtml(title)}</title>
|
|
1071
|
+
<style>
|
|
1072
|
+
html, body, #app-shell, #app { width: 100%; height: 100%; margin: 0; overflow: hidden; }
|
|
1073
|
+
body { background: #05070b; }
|
|
1074
|
+
#app { display: block; }
|
|
1075
|
+
#app-shell { position: relative; }
|
|
1076
|
+
#game-ui { position: absolute; inset: 0; overflow: hidden; pointer-events: none; }
|
|
1077
|
+
#game-ui > * { pointer-events: auto; }
|
|
1078
|
+
#forgeax-fatal { position: fixed; inset: 0; z-index: 2147483647; display: none; place-items: center; padding: 24px; background: #0b0d10; color: #e6e6e6; font: 15px/1.6 system-ui, sans-serif; text-align: center; white-space: pre-wrap; }
|
|
1079
|
+
</style>
|
|
1080
|
+
</head>
|
|
1081
|
+
<body>
|
|
1082
|
+
<div id="app-shell"><canvas id="app"></canvas><div id="game-ui"></div></div><div id="forgeax-fatal" role="alert"></div>
|
|
1083
|
+
<script>
|
|
1084
|
+
(() => {
|
|
1085
|
+
const show = (reason) => {
|
|
1086
|
+
const notice = document.querySelector('#forgeax-fatal');
|
|
1087
|
+
if (!(notice instanceof HTMLElement)) return;
|
|
1088
|
+
const message = reason instanceof Error ? reason.message : String(reason ?? 'Unknown startup failure');
|
|
1089
|
+
notice.textContent = 'ForgeaX game failed to start.\\n' + message;
|
|
1090
|
+
notice.style.display = 'grid';
|
|
1091
|
+
};
|
|
1092
|
+
window.addEventListener('error', (event) => show(event.error ?? event.message));
|
|
1093
|
+
window.addEventListener('unhandledrejection', (event) => show(event.reason));
|
|
1094
|
+
})();
|
|
1095
|
+
</script>
|
|
1096
|
+
<script type="module" src="./main.ts"></script>
|
|
1097
|
+
</body>
|
|
1098
|
+
</html>
|
|
1099
|
+
`;
|
|
1100
|
+
}
|
|
1101
|
+
async function createViteConfig(facts, command2, base = "/", options = {}) {
|
|
1102
|
+
const bootstrapRoot = options.bootstrapRoot ?? "project-bootstrap";
|
|
1103
|
+
const canonicalKit = bootstrapRoot === "resource-bootstrap" ? await resolveCanonicalKit() : void 0;
|
|
1104
|
+
const generated = resolve(facts.root, ".forgeax", "generated");
|
|
1105
|
+
await mkdir(generated, { recursive: true });
|
|
1106
|
+
await Promise.all([
|
|
1107
|
+
writeFile(resolve(generated, "index.html"), htmlSource(facts.name)),
|
|
1108
|
+
writeFile(resolve(generated, "main.ts"), hostSource(facts, bootstrapRoot, canonicalKit?.guid))
|
|
1109
|
+
]);
|
|
1110
|
+
const roots = [
|
|
1111
|
+
...facts.assetRoots.map((root) => resolve(facts.root, root)),
|
|
1112
|
+
...canonicalKit === void 0 ? [] : [canonicalKit.root]
|
|
1113
|
+
];
|
|
1114
|
+
const importers = [...projectImporters(facts)];
|
|
1115
|
+
const runtimeBinding = createStandaloneRuntimeAssetBinding(facts.id);
|
|
1116
|
+
const engineWorkspaceResolver = await createEngineWorkspaceResolver();
|
|
1117
|
+
const plugins = [
|
|
1118
|
+
...engineWorkspaceResolver === void 0 ? [] : [engineWorkspaceResolver],
|
|
1119
|
+
forgeaxShader(),
|
|
1120
|
+
pluginPack({
|
|
1121
|
+
roots,
|
|
1122
|
+
base,
|
|
1123
|
+
runtimeBinding,
|
|
1124
|
+
ddc: devKitDdcRoots(facts.root),
|
|
1125
|
+
scriptablePack: {},
|
|
1126
|
+
refresh: command2 === "serve" ? reloadAssetHost() : void 0,
|
|
1127
|
+
importers: [
|
|
1128
|
+
audioImporter,
|
|
1129
|
+
imageImporter,
|
|
1130
|
+
fbxImporter,
|
|
1131
|
+
gltfImporter,
|
|
1132
|
+
fontImporter,
|
|
1133
|
+
...importers
|
|
1134
|
+
],
|
|
1135
|
+
cookers: [createParticleCodeNativeCookerFromRoots(roots)],
|
|
1136
|
+
...bootstrapRoot === "resource-bootstrap" ? { ignorePath: isResourcePreviewIgnoredPath } : { ignorePath: isProjectSourceIgnoredPath }
|
|
1137
|
+
})
|
|
1138
|
+
];
|
|
1139
|
+
return {
|
|
1140
|
+
root: generated,
|
|
1141
|
+
base,
|
|
1142
|
+
configFile: false,
|
|
1143
|
+
publicDir: facts.assetPublicDir === void 0 ? false : resolve(facts.root, facts.assetPublicDir),
|
|
1144
|
+
plugins,
|
|
1145
|
+
resolve: { dedupe: ["@forgeax/engine-app", "@forgeax/engine-ecs", "@forgeax/engine-runtime"] },
|
|
1146
|
+
server: { fs: { allow: [facts.root, ...roots] } },
|
|
1147
|
+
build: {
|
|
1148
|
+
target: "esnext",
|
|
1149
|
+
outDir: options.outDir ?? resolve(facts.root, "dist"),
|
|
1150
|
+
emptyOutDir: true,
|
|
1151
|
+
rollupOptions: { input: resolve(generated, "index.html") }
|
|
1152
|
+
}
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
var hostRequire, engineWorkspacePackagesPromise;
|
|
1156
|
+
var init_host = __esm({
|
|
1157
|
+
"src/host.ts"() {
|
|
1158
|
+
hostRequire = createRequire(import.meta.url);
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
1161
|
+
function projectError(code, expected, hint, detail) {
|
|
1162
|
+
return { ok: false, error: { code, expected, hint, detail } };
|
|
1163
|
+
}
|
|
1164
|
+
async function readJson(path) {
|
|
1165
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
1166
|
+
}
|
|
1167
|
+
function firstUnsupportedStandaloneRealm(entries2, inheritedRealm = "engine") {
|
|
1168
|
+
for (const entry of entries2) {
|
|
1169
|
+
const realm = entry.realm ?? inheritedRealm ?? "engine";
|
|
1170
|
+
if (realm === "host") return { id: entry.id, realm };
|
|
1171
|
+
if (entry.group === true) {
|
|
1172
|
+
const unsupported = firstUnsupportedStandaloneRealm(
|
|
1173
|
+
entry.config,
|
|
1174
|
+
realm
|
|
1175
|
+
);
|
|
1176
|
+
if (unsupported !== void 0) return unsupported;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
return void 0;
|
|
1180
|
+
}
|
|
1181
|
+
function pluginModuleNames(entries2) {
|
|
1182
|
+
return entries2.flatMap(
|
|
1183
|
+
(entry) => entry.group === true ? pluginModuleNames(entry.config) : [entry.name]
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
async function readProjectFacts(rootInput = process.cwd()) {
|
|
1187
|
+
const root = resolve(rootInput);
|
|
1188
|
+
let forgeValue;
|
|
1189
|
+
let packageValue;
|
|
1190
|
+
try {
|
|
1191
|
+
[forgeValue, packageValue] = await Promise.all([
|
|
1192
|
+
readJson(resolve(root, "forge.json")),
|
|
1193
|
+
readJson(resolve(root, "package.json"))
|
|
1194
|
+
]);
|
|
1195
|
+
} catch (cause) {
|
|
1196
|
+
return projectError(
|
|
1197
|
+
"project-manifest-unreadable",
|
|
1198
|
+
"readable forge.json and package.json files",
|
|
1199
|
+
"Run the command from a ForgeaX game root or pass its directory.",
|
|
1200
|
+
{ root, reason: cause instanceof Error ? cause.message : String(cause) }
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
if (forgeValue === null || typeof forgeValue !== "object") {
|
|
1204
|
+
return projectError(
|
|
1205
|
+
"project-manifest-invalid",
|
|
1206
|
+
"forge.json to contain an object",
|
|
1207
|
+
"Repair forge.json before running DevKit.",
|
|
1208
|
+
{ root }
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
if (packageValue === null || typeof packageValue !== "object") {
|
|
1212
|
+
return projectError(
|
|
1213
|
+
"package-manifest-invalid",
|
|
1214
|
+
"package.json to contain an object",
|
|
1215
|
+
"Repair package.json before running DevKit.",
|
|
1216
|
+
{ root }
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
1219
|
+
const parsedForge = GameProjectSchema.safeParse(forgeValue);
|
|
1220
|
+
if (!parsedForge.success) {
|
|
1221
|
+
return projectError(
|
|
1222
|
+
"project-manifest-invalid",
|
|
1223
|
+
"forge.json to satisfy @forgeax/engine-project GameProjectSchema",
|
|
1224
|
+
"Repair the fields reported by the authoritative project schema.",
|
|
1225
|
+
{ root, issues: parsedForge.error.issues }
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
const forge = parsedForge.data;
|
|
1229
|
+
if (forge.id.length === 0 || forge.name.length === 0 || forge.entry === void 0 || forge.entry.length === 0) {
|
|
1230
|
+
return projectError(
|
|
1231
|
+
"project-manifest-invalid",
|
|
1232
|
+
"forge.json to declare id, name, and entry",
|
|
1233
|
+
"Restore the project entry; plugin Entries remain optional additions.",
|
|
1234
|
+
{ root }
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
const plugins = forge.plugins ?? [];
|
|
1238
|
+
const unsupportedRealm = firstUnsupportedStandaloneRealm(plugins);
|
|
1239
|
+
if (unsupportedRealm !== void 0) {
|
|
1240
|
+
return projectError(
|
|
1241
|
+
"project-plugin-realm-unsupported",
|
|
1242
|
+
"the standalone Devkit host to contain only engine-realm plugin Entries",
|
|
1243
|
+
"Move Host or build plugins to a host that owns that physical realm.",
|
|
1244
|
+
{ root, ...unsupportedRealm }
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
const entryPath = isAbsolute(forge.entry) ? forge.entry : resolve(root, forge.entry);
|
|
1248
|
+
try {
|
|
1249
|
+
await readFile(entryPath);
|
|
1250
|
+
} catch {
|
|
1251
|
+
return projectError(
|
|
1252
|
+
"project-entry-missing",
|
|
1253
|
+
"forge.json#entry to resolve to a readable module",
|
|
1254
|
+
"Restore the game entry or update forge.json#entry.",
|
|
1255
|
+
{ root, entry: forge.entry }
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
const packageJson = packageValue;
|
|
1259
|
+
const forgeax = packageJson.forgeax;
|
|
1260
|
+
const configuredRoots = forgeax !== null && typeof forgeax === "object" ? forgeax.assets?.roots : void 0;
|
|
1261
|
+
const assetRoots = Array.isArray(configuredRoots) && configuredRoots.every((value) => typeof value === "string") ? configuredRoots : ["assets"];
|
|
1262
|
+
const configuredImporters = forgeax !== null && typeof forgeax === "object" ? forgeax.assets?.importers : void 0;
|
|
1263
|
+
const assetImporters = Array.isArray(configuredImporters) && configuredImporters.every((value) => typeof value === "string") ? configuredImporters : [];
|
|
1264
|
+
const configuredPublicDir = forgeax !== null && typeof forgeax === "object" ? forgeax.assets?.publicDir : void 0;
|
|
1265
|
+
const assetPublicDir = typeof configuredPublicDir === "string" ? configuredPublicDir : void 0;
|
|
1266
|
+
const physics = forge.physics === "2d" || forge.physics === "3d" ? forge.physics : void 0;
|
|
1267
|
+
const defaultScene = typeof forge.defaultScene === "string" && forge.defaultScene.length > 0 ? forge.defaultScene : void 0;
|
|
1268
|
+
const normalizedEntry = forge.entry.startsWith("./") ? forge.entry : `./${forge.entry}`;
|
|
1269
|
+
const bootstrapEntry = pluginModuleNames(plugins).map((name) => name.startsWith("./") ? name : `./${name}`).includes(normalizedEntry) ? void 0 : forge.entry;
|
|
1270
|
+
return {
|
|
1271
|
+
ok: true,
|
|
1272
|
+
value: {
|
|
1273
|
+
root,
|
|
1274
|
+
id: forge.id,
|
|
1275
|
+
name: forge.name,
|
|
1276
|
+
entry: forge.entry,
|
|
1277
|
+
...bootstrapEntry === void 0 ? {} : { bootstrapEntry },
|
|
1278
|
+
plugins,
|
|
1279
|
+
...physics === void 0 ? {} : { physics },
|
|
1280
|
+
...defaultScene === void 0 ? {} : { defaultScene },
|
|
1281
|
+
assetRoots,
|
|
1282
|
+
...assetImporters.length === 0 ? {} : { assetImporters },
|
|
1283
|
+
...assetPublicDir === void 0 ? {} : { assetPublicDir },
|
|
1284
|
+
packageJson
|
|
1285
|
+
}
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
function isCommandError(value) {
|
|
1289
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1290
|
+
const candidate = value;
|
|
1291
|
+
return typeof candidate.code === "string" && typeof candidate.expected === "string" && typeof candidate.hint === "string" && candidate.detail !== null && typeof candidate.detail === "object" && !Array.isArray(candidate.detail);
|
|
1292
|
+
}
|
|
1293
|
+
function commandError(cause, fallbackCode) {
|
|
1294
|
+
if (isCommandError(cause)) return cause;
|
|
1295
|
+
return {
|
|
1296
|
+
code: fallbackCode,
|
|
1297
|
+
expected: "the ForgeaX command to complete",
|
|
1298
|
+
hint: "Inspect the underlying diagnostic and repair the owning input.",
|
|
1299
|
+
detail: { reason: cause instanceof Error ? cause.message : String(cause) }
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
var init_project = __esm({
|
|
1303
|
+
"src/project.ts"() {
|
|
1304
|
+
}
|
|
1305
|
+
});
|
|
1306
|
+
function failure(error) {
|
|
1307
|
+
return { ok: false, error };
|
|
1308
|
+
}
|
|
1309
|
+
function producerFailure(stderr, fallback, detail) {
|
|
1310
|
+
try {
|
|
1311
|
+
const parsed = JSON.parse(stderr.at(-1) ?? "");
|
|
1312
|
+
if (isCommandError(parsed)) return failure(parsed);
|
|
1313
|
+
} catch {
|
|
1314
|
+
}
|
|
1315
|
+
return failure({ ...fallback, detail: { ...detail, diagnostic: stderr.join("\n") } });
|
|
1316
|
+
}
|
|
1317
|
+
async function sourcesAt(path) {
|
|
1318
|
+
const info = await stat(path);
|
|
1319
|
+
if (info.isFile()) return [path];
|
|
1320
|
+
if (!info.isDirectory()) return [];
|
|
1321
|
+
const output = [];
|
|
1322
|
+
const entries2 = await readdir(path, { withFileTypes: true });
|
|
1323
|
+
for (const entry of entries2.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
1324
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist")
|
|
1325
|
+
continue;
|
|
1326
|
+
const child = resolve(path, entry.name);
|
|
1327
|
+
if (entry.isDirectory()) output.push(...await sourcesAt(child));
|
|
1328
|
+
else if (entry.isFile() && !entry.name.endsWith(".meta.json")) output.push(child);
|
|
1329
|
+
}
|
|
1330
|
+
return output;
|
|
1331
|
+
}
|
|
1332
|
+
async function addImage(sourcePath, dryRun2) {
|
|
1333
|
+
const metaPath = `${sourcePath}.meta.json`;
|
|
1334
|
+
const source = basename(sourcePath);
|
|
1335
|
+
let existing;
|
|
1336
|
+
try {
|
|
1337
|
+
existing = JSON.parse(await readFile(metaPath, "utf8"));
|
|
1338
|
+
} catch (cause) {
|
|
1339
|
+
if (cause.code !== "ENOENT") {
|
|
1340
|
+
return failure({
|
|
1341
|
+
code: "asset-meta-unreadable",
|
|
1342
|
+
expected: "an absent or readable JSON sidecar",
|
|
1343
|
+
hint: "Repair the existing sidecar before adding the source again.",
|
|
1344
|
+
detail: { source: sourcePath, metaPath }
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
if (existing !== void 0) {
|
|
1349
|
+
const value = existing;
|
|
1350
|
+
const row = value.subAssets?.[0];
|
|
1351
|
+
if (value.importer !== "image" || value.source !== source || value.subAssets?.length !== 1 || typeof row?.guid !== "string" || row.kind !== "texture") {
|
|
1352
|
+
return failure({
|
|
1353
|
+
code: "asset-meta-conflict",
|
|
1354
|
+
expected: "the existing sidecar to describe this image source and one texture identity",
|
|
1355
|
+
hint: "Resolve the sidecar conflict explicitly; DevKit will not replace authored identity.",
|
|
1356
|
+
detail: { source: sourcePath, metaPath }
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
return { ok: true, value: { source: sourcePath, metaPath, guid: row.guid, reused: true } };
|
|
1360
|
+
}
|
|
1361
|
+
const guid = AssetGuid.format(AssetGuid.random());
|
|
1362
|
+
const linear = extname(sourcePath).toLowerCase() === ".hdr";
|
|
1363
|
+
const meta = {
|
|
1364
|
+
schemaVersion: "1.0.0",
|
|
1365
|
+
kind: "external-asset-package",
|
|
1366
|
+
importer: "image",
|
|
1367
|
+
source,
|
|
1368
|
+
importSettings: {
|
|
1369
|
+
colorSpace: linear ? "linear" : "srgb",
|
|
1370
|
+
mipmap: true,
|
|
1371
|
+
addressMode: "repeat",
|
|
1372
|
+
filterMode: "linear"
|
|
1373
|
+
},
|
|
1374
|
+
subAssets: [{ guid, sourceIndex: 0, kind: "texture", sourceKey: "texture" }]
|
|
1375
|
+
};
|
|
1376
|
+
if (!dryRun2) await writeFile(metaPath, `${JSON.stringify(meta, null, 2)}
|
|
1377
|
+
`, { flag: "wx" });
|
|
1378
|
+
return { ok: true, value: { source: sourcePath, metaPath, guid, reused: false, dryRun: dryRun2 } };
|
|
1379
|
+
}
|
|
1380
|
+
async function addGltf(sourcePath, dryRun2) {
|
|
1381
|
+
if (dryRun2) {
|
|
1382
|
+
const exists = await stat(`${sourcePath}.meta.json`).then(() => true).catch(() => false);
|
|
1383
|
+
return {
|
|
1384
|
+
ok: true,
|
|
1385
|
+
value: {
|
|
1386
|
+
source: sourcePath,
|
|
1387
|
+
metaPath: `${sourcePath}.meta.json`,
|
|
1388
|
+
reused: exists,
|
|
1389
|
+
dryRun: true
|
|
1390
|
+
}
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
const stdout = [];
|
|
1394
|
+
const stderr = [];
|
|
1395
|
+
const exitCode = await runCliGltf(["import", sourcePath], {
|
|
1396
|
+
stdoutWrite: (line) => stdout.push(line),
|
|
1397
|
+
stderrWrite: (line) => stderr.push(line)
|
|
1398
|
+
});
|
|
1399
|
+
if (exitCode !== 0) {
|
|
1400
|
+
return producerFailure(
|
|
1401
|
+
stderr,
|
|
1402
|
+
{
|
|
1403
|
+
code: "asset-add-failed",
|
|
1404
|
+
expected: "the glTF producer to create or reuse a valid sidecar",
|
|
1405
|
+
hint: "Inspect the glTF source and its external references."
|
|
1406
|
+
},
|
|
1407
|
+
{ source: sourcePath }
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
return { ok: true, value: { source: sourcePath, metaPath: `${sourcePath}.meta.json` } };
|
|
1411
|
+
}
|
|
1412
|
+
async function assetAddCommand(options) {
|
|
1413
|
+
const facts = await readProjectFacts(options.root);
|
|
1414
|
+
if (!facts.ok) return facts;
|
|
1415
|
+
const target = resolve(facts.value.root, options.path);
|
|
1416
|
+
try {
|
|
1417
|
+
const sources = await sourcesAt(target);
|
|
1418
|
+
const supported = sources.filter((source) => {
|
|
1419
|
+
const extension = extname(source).toLowerCase();
|
|
1420
|
+
return imageExtensions.has(extension) || gltfExtensions.has(extension);
|
|
1421
|
+
});
|
|
1422
|
+
if (supported.length === 0) {
|
|
1423
|
+
return failure({
|
|
1424
|
+
code: "source-package-importer-missing",
|
|
1425
|
+
expected: "a .png, .jpg, .jpeg, .hdr, .gltf, or .glb source",
|
|
1426
|
+
hint: "Use a supported built-in importer or add an explicit producer before adding this source.",
|
|
1427
|
+
detail: { target }
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
const assets = [];
|
|
1431
|
+
for (const source of supported) {
|
|
1432
|
+
const result = imageExtensions.has(extname(source).toLowerCase()) ? await addImage(source, options.dryRun === true) : await addGltf(source, options.dryRun === true);
|
|
1433
|
+
if (!result.ok) return result;
|
|
1434
|
+
assets.push(result.value);
|
|
1435
|
+
}
|
|
1436
|
+
return { ok: true, value: { root: facts.value.root, assets, dryRun: options.dryRun === true } };
|
|
1437
|
+
} catch (cause) {
|
|
1438
|
+
return failure(commandError(cause, "asset-add-failed"));
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
async function entries(options) {
|
|
1442
|
+
const facts = await readProjectFacts(options.root);
|
|
1443
|
+
if (!facts.ok) return facts;
|
|
1444
|
+
const stdout = [];
|
|
1445
|
+
const stderr = [];
|
|
1446
|
+
const result = await scanEntries(
|
|
1447
|
+
facts.value.assetRoots.map((root) => resolve(facts.value.root, root)),
|
|
1448
|
+
{ stdoutWrite: (line) => stdout.push(line), stderrWrite: (line) => stderr.push(line) }
|
|
1449
|
+
);
|
|
1450
|
+
if (!result.ok) {
|
|
1451
|
+
return producerFailure(
|
|
1452
|
+
stderr,
|
|
1453
|
+
{
|
|
1454
|
+
code: "asset-authority-invalid",
|
|
1455
|
+
expected: "all asset roots and sidecars to pass the pack scanner",
|
|
1456
|
+
hint: "Repair the first invalid asset authority reported by the scanner."
|
|
1457
|
+
},
|
|
1458
|
+
{}
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
return { ok: true, value: { facts: facts.value, entries: result.value } };
|
|
1462
|
+
}
|
|
1463
|
+
async function assetListCommand(options = {}) {
|
|
1464
|
+
const result = await entries(options);
|
|
1465
|
+
if (!result.ok) return result;
|
|
1466
|
+
return { ok: true, value: result.value.entries };
|
|
1467
|
+
}
|
|
1468
|
+
async function assetVerifyCommand(options = {}) {
|
|
1469
|
+
const result = await entries(options);
|
|
1470
|
+
if (!result.ok) return result;
|
|
1471
|
+
return {
|
|
1472
|
+
ok: true,
|
|
1473
|
+
value: { root: result.value.facts.root, assetCount: result.value.entries.length }
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
async function assetInspectCommand(options) {
|
|
1477
|
+
const result = await entries(options);
|
|
1478
|
+
if (!result.ok) return result;
|
|
1479
|
+
const subject = options.subject.toLowerCase();
|
|
1480
|
+
const matches = result.value.entries.filter(
|
|
1481
|
+
(entry) => entry.guid.toLowerCase() === subject || entry.name?.toLowerCase() === subject
|
|
1482
|
+
);
|
|
1483
|
+
if (matches.length !== 1) {
|
|
1484
|
+
return failure({
|
|
1485
|
+
code: matches.length === 0 ? "asset-not-found" : "asset-subject-ambiguous",
|
|
1486
|
+
expected: "the GUID or name to resolve to exactly one asset",
|
|
1487
|
+
hint: matches.length === 0 ? "Run asset list and choose a known subject." : "Use the stable GUID.",
|
|
1488
|
+
detail: { subject: options.subject, matches }
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
return { ok: true, value: matches[0] };
|
|
1492
|
+
}
|
|
1493
|
+
var imageExtensions, gltfExtensions;
|
|
1494
|
+
var init_assets = __esm({
|
|
1495
|
+
"src/assets.ts"() {
|
|
1496
|
+
init_project();
|
|
1497
|
+
imageExtensions = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".hdr"]);
|
|
1498
|
+
gltfExtensions = /* @__PURE__ */ new Set([".gltf", ".glb"]);
|
|
1499
|
+
}
|
|
1500
|
+
});
|
|
1501
|
+
|
|
1502
|
+
// package.json
|
|
1503
|
+
var package_default;
|
|
1504
|
+
var init_package = __esm({
|
|
1505
|
+
"package.json"() {
|
|
1506
|
+
package_default = {
|
|
1507
|
+
name: "@forgeax/engine-devkit",
|
|
1508
|
+
version: "0.0.0",
|
|
1509
|
+
private: false,
|
|
1510
|
+
type: "module",
|
|
1511
|
+
license: "Apache-2.0",
|
|
1512
|
+
sideEffects: false,
|
|
1513
|
+
description: "ForgeaX external game project CLI and build orchestration.",
|
|
1514
|
+
bin: {
|
|
1515
|
+
forgeax: "./dist/cli.mjs"
|
|
1516
|
+
},
|
|
1517
|
+
exports: {
|
|
1518
|
+
".": {
|
|
1519
|
+
types: "./dist/index.d.ts",
|
|
1520
|
+
import: "./dist/index.mjs"
|
|
1521
|
+
},
|
|
1522
|
+
"./cli": "./dist/cli.mjs",
|
|
1523
|
+
"./package.json": "./package.json"
|
|
1524
|
+
},
|
|
1525
|
+
main: "./dist/index.mjs",
|
|
1526
|
+
types: "./dist/index.d.ts",
|
|
1527
|
+
files: [
|
|
1528
|
+
"dist",
|
|
1529
|
+
"README.md",
|
|
1530
|
+
"LICENSE"
|
|
1531
|
+
],
|
|
1532
|
+
scripts: {
|
|
1533
|
+
build: "tsup",
|
|
1534
|
+
test: "vitest run"
|
|
1535
|
+
},
|
|
1536
|
+
dependencies: {
|
|
1537
|
+
"@forgeax/engine-animation": "workspace:*",
|
|
1538
|
+
"@forgeax/engine-app": "workspace:*",
|
|
1539
|
+
"@forgeax/engine-assets-runtime": "workspace:*",
|
|
1540
|
+
"@forgeax/engine-audio": "workspace:*",
|
|
1541
|
+
"@forgeax/engine-audio-webaudio": "workspace:*",
|
|
1542
|
+
"@forgeax/engine-fbx": "workspace:*",
|
|
1543
|
+
"@forgeax/engine-font": "workspace:*",
|
|
1544
|
+
"@forgeax/engine-geometry": "workspace:*",
|
|
1545
|
+
"@forgeax/engine-gltf": "workspace:*",
|
|
1546
|
+
"@forgeax/engine-image": "workspace:*",
|
|
1547
|
+
"@forgeax/engine-input": "workspace:*",
|
|
1548
|
+
"@forgeax/engine-pack": "workspace:*",
|
|
1549
|
+
"@forgeax/engine-physics": "workspace:*",
|
|
1550
|
+
"@forgeax/engine-plugin": "workspace:*",
|
|
1551
|
+
"@forgeax/engine-preview": "workspace:*",
|
|
1552
|
+
"@forgeax/engine-project": "workspace:*",
|
|
1553
|
+
"@forgeax/engine-rhi-debug": "workspace:*",
|
|
1554
|
+
"@forgeax/engine-render": "workspace:*",
|
|
1555
|
+
"@forgeax/engine-render-graph": "workspace:*",
|
|
1556
|
+
"@forgeax/engine-runtime": "workspace:*",
|
|
1557
|
+
"@forgeax/engine-scene": "workspace:*",
|
|
1558
|
+
"@forgeax/engine-shader-compiler": "workspace:*",
|
|
1559
|
+
"@forgeax/engine-skinning": "workspace:*",
|
|
1560
|
+
"@forgeax/engine-tool-runtime": "workspace:*",
|
|
1561
|
+
"@forgeax/engine-types": "workspace:*",
|
|
1562
|
+
"@forgeax/engine-vfx-compiler": "workspace:*",
|
|
1563
|
+
"@forgeax/engine-vite-plugin-pack": "workspace:*",
|
|
1564
|
+
"@forgeax/engine-vite-plugin-shader": "workspace:*",
|
|
1565
|
+
jiti: "1.21.7",
|
|
1566
|
+
playwright: "1.60.0",
|
|
1567
|
+
vite: "8.0.10",
|
|
1568
|
+
vitest: "4.1.5"
|
|
1569
|
+
},
|
|
1570
|
+
devDependencies: {
|
|
1571
|
+
"@forgeax/engine-rhi-null": "workspace:*",
|
|
1572
|
+
"@types/node": "^20.14.0"
|
|
1573
|
+
},
|
|
1574
|
+
forgeax: {
|
|
1575
|
+
metrics: {
|
|
1576
|
+
"bundle-size": {
|
|
1577
|
+
enabled: true,
|
|
1578
|
+
path: "dist/cli.mjs",
|
|
1579
|
+
compression: "gzip"
|
|
1580
|
+
},
|
|
1581
|
+
fps: {
|
|
1582
|
+
enabled: false,
|
|
1583
|
+
reason: "Node-only build orchestration; runtime FPS belongs to consumer games"
|
|
1584
|
+
},
|
|
1585
|
+
bench: {
|
|
1586
|
+
enabled: false,
|
|
1587
|
+
reason: "CLI latency is not a runtime hot path"
|
|
1588
|
+
},
|
|
1589
|
+
gate: {
|
|
1590
|
+
enabled: true,
|
|
1591
|
+
command: "pnpm test",
|
|
1592
|
+
cwd: "packages/devkit"
|
|
1593
|
+
},
|
|
1594
|
+
"spike-report": {
|
|
1595
|
+
enabled: false,
|
|
1596
|
+
reason: "production DevKit package"
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
});
|
|
1603
|
+
function createInitPlan(facts, sdk) {
|
|
1604
|
+
const manifest = structuredClone(facts.packageJson);
|
|
1605
|
+
const packageVersions = new Map(sdk?.packages.map((entry) => [entry.name, entry.version]));
|
|
1606
|
+
const devkitVersion = packageVersions.get("@forgeax/engine-devkit") ?? package_default.version;
|
|
1607
|
+
const dependencyChanges = [];
|
|
1608
|
+
if (sdk !== void 0) {
|
|
1609
|
+
for (const section of ["dependencies", "devDependencies"]) {
|
|
1610
|
+
const dependencies = manifest[section];
|
|
1611
|
+
if (dependencies === null || typeof dependencies !== "object") continue;
|
|
1612
|
+
const unsupported = Object.keys(dependencies).filter(
|
|
1613
|
+
(name) => !name.startsWith("@forgeax/engine-") && !(name in SDK_DEV_DEPENDENCIES)
|
|
1614
|
+
);
|
|
1615
|
+
if (unsupported.length > 0) {
|
|
1616
|
+
return {
|
|
1617
|
+
ok: false,
|
|
1618
|
+
error: {
|
|
1619
|
+
code: "sdk-external-dependency-unsupported",
|
|
1620
|
+
expected: "archive-backed init dependencies to belong to the SDK closure",
|
|
1621
|
+
hint: "Remove the external dependency or use a network-backed package workflow explicitly.",
|
|
1622
|
+
detail: { root: facts.root, dependencies: unsupported.sort() }
|
|
1623
|
+
}
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
for (const section of ["dependencies", "devDependencies"]) {
|
|
1629
|
+
const dependencies = manifest[section];
|
|
1630
|
+
if (dependencies === null || typeof dependencies !== "object") continue;
|
|
1631
|
+
for (const [name, value] of Object.entries(dependencies)) {
|
|
1632
|
+
if (name.startsWith("@forgeax/engine-") && typeof value === "string" && value.startsWith("workspace:")) {
|
|
1633
|
+
const version = packageVersions.get(name) ?? devkitVersion;
|
|
1634
|
+
dependencyChanges.push({ section, name, from: value, to: version });
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
if (sdk !== void 0) {
|
|
1639
|
+
const existingEngineNames = new Set(
|
|
1640
|
+
["dependencies", "devDependencies"].flatMap((section) => {
|
|
1641
|
+
const value = manifest[section];
|
|
1642
|
+
return value !== null && typeof value === "object" ? Object.keys(value).filter((name) => name.startsWith("@forgeax/engine-")) : [];
|
|
1643
|
+
})
|
|
1644
|
+
);
|
|
1645
|
+
for (const entry of sdk.packages) {
|
|
1646
|
+
if (entry.name === "@forgeax/engine-devkit" || existingEngineNames.has(entry.name)) continue;
|
|
1647
|
+
dependencyChanges.push({
|
|
1648
|
+
section: "dependencies",
|
|
1649
|
+
name: entry.name,
|
|
1650
|
+
to: entry.version
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
const devDependencies2 = manifest.devDependencies !== null && typeof manifest.devDependencies === "object" ? manifest.devDependencies : {};
|
|
1654
|
+
for (const [name, version] of Object.entries(SDK_DEV_DEPENDENCIES)) {
|
|
1655
|
+
if (devDependencies2[name] === version) continue;
|
|
1656
|
+
dependencyChanges.push({
|
|
1657
|
+
section: "devDependencies",
|
|
1658
|
+
name,
|
|
1659
|
+
...typeof devDependencies2[name] === "string" ? { from: devDependencies2[name] } : {},
|
|
1660
|
+
to: version
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
const scriptsValue = manifest.scripts;
|
|
1665
|
+
const scripts = scriptsValue !== null && typeof scriptsValue === "object" ? scriptsValue : {};
|
|
1666
|
+
const scriptChanges = [];
|
|
1667
|
+
for (const [name, command2] of Object.entries(STANDARD_SCRIPTS)) {
|
|
1668
|
+
const existing = scripts[name];
|
|
1669
|
+
if (existing === void 0) scriptChanges.push({ name, to: command2 });
|
|
1670
|
+
else if (existing !== command2) {
|
|
1671
|
+
return {
|
|
1672
|
+
ok: false,
|
|
1673
|
+
error: {
|
|
1674
|
+
code: "project-script-conflict",
|
|
1675
|
+
expected: `package.json#scripts.${name} to be absent or ${JSON.stringify(command2)}`,
|
|
1676
|
+
hint: `Rename the existing ${name} script, then rerun forgeax init.`,
|
|
1677
|
+
detail: { root: facts.root, script: name, existing }
|
|
1678
|
+
}
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
const devDependencies = manifest.devDependencies;
|
|
1683
|
+
const existingDevkit = devDependencies !== null && typeof devDependencies === "object" ? devDependencies["@forgeax/engine-devkit"] : void 0;
|
|
1684
|
+
if (existingDevkit !== devkitVersion) {
|
|
1685
|
+
dependencyChanges.push({
|
|
1686
|
+
section: "devDependencies",
|
|
1687
|
+
name: "@forgeax/engine-devkit",
|
|
1688
|
+
...typeof existingDevkit === "string" ? { from: existingDevkit } : {},
|
|
1689
|
+
to: devkitVersion
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
return {
|
|
1693
|
+
ok: true,
|
|
1694
|
+
value: {
|
|
1695
|
+
root: facts.root,
|
|
1696
|
+
version: sdk?.sdkVersion ?? devkitVersion,
|
|
1697
|
+
archiveBacked: sdk !== void 0,
|
|
1698
|
+
dependencyChanges,
|
|
1699
|
+
scriptChanges
|
|
1700
|
+
}
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
async function applyInitPlan(facts, plan, options) {
|
|
1704
|
+
if (options.dryRun === true) return { ok: true, value: plan };
|
|
1705
|
+
const manifest = structuredClone(facts.packageJson);
|
|
1706
|
+
for (const change of plan.dependencyChanges) {
|
|
1707
|
+
const sectionValue = manifest[change.section];
|
|
1708
|
+
const section = sectionValue !== null && typeof sectionValue === "object" ? sectionValue : {};
|
|
1709
|
+
section[change.name] = change.to;
|
|
1710
|
+
manifest[change.section] = section;
|
|
1711
|
+
}
|
|
1712
|
+
const scriptsValue = manifest.scripts;
|
|
1713
|
+
const scripts = scriptsValue !== null && typeof scriptsValue === "object" ? scriptsValue : {};
|
|
1714
|
+
for (const change of plan.scriptChanges) scripts[change.name] = change.to;
|
|
1715
|
+
manifest.scripts = scripts;
|
|
1716
|
+
if (plan.archiveBacked) {
|
|
1717
|
+
manifest.packageManager = "pnpm@10.33.2";
|
|
1718
|
+
const pnpmValue = manifest.pnpm;
|
|
1719
|
+
const pnpm = pnpmValue !== null && typeof pnpmValue === "object" ? pnpmValue : {};
|
|
1720
|
+
manifest.pnpm = {
|
|
1721
|
+
...pnpm,
|
|
1722
|
+
onlyBuiltDependencies: [
|
|
1723
|
+
"@forgeax/engine-codec",
|
|
1724
|
+
"@forgeax/engine-fbx",
|
|
1725
|
+
"@forgeax/engine-wgpu-wasm",
|
|
1726
|
+
"esbuild"
|
|
1727
|
+
],
|
|
1728
|
+
supportedArchitectures: {
|
|
1729
|
+
os: ["darwin", "linux", "win32"],
|
|
1730
|
+
cpu: ["x64", "arm64"],
|
|
1731
|
+
libc: ["glibc", "musl"]
|
|
1732
|
+
}
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
await writeFile(resolve(facts.root, "package.json"), `${JSON.stringify(manifest, null, 2)}
|
|
1736
|
+
`);
|
|
1737
|
+
return { ok: true, value: plan };
|
|
1738
|
+
}
|
|
1739
|
+
var STANDARD_SCRIPTS, SDK_DEV_DEPENDENCIES;
|
|
1740
|
+
var init_init = __esm({
|
|
1741
|
+
"src/init.ts"() {
|
|
1742
|
+
init_package();
|
|
1743
|
+
STANDARD_SCRIPTS = {
|
|
1744
|
+
dev: "forgeax dev",
|
|
1745
|
+
build: "forgeax build",
|
|
1746
|
+
package: "forgeax package",
|
|
1747
|
+
serve: "forgeax serve",
|
|
1748
|
+
preview: "forgeax preview",
|
|
1749
|
+
doctor: "forgeax doctor",
|
|
1750
|
+
test: "forgeax test"
|
|
1751
|
+
};
|
|
1752
|
+
SDK_DEV_DEPENDENCIES = {
|
|
1753
|
+
"@types/node": "20.19.40",
|
|
1754
|
+
"@webgpu/types": "0.1.71",
|
|
1755
|
+
tsx: "4.23.1",
|
|
1756
|
+
typescript: "6.0.3",
|
|
1757
|
+
vitest: "4.1.5"
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
});
|
|
1761
|
+
async function readable(path) {
|
|
1762
|
+
try {
|
|
1763
|
+
await access(path);
|
|
1764
|
+
return true;
|
|
1765
|
+
} catch {
|
|
1766
|
+
return false;
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
async function findSdkContext() {
|
|
1770
|
+
const configured = process.env.FORGEAX_SDK_ROOT;
|
|
1771
|
+
let cursor = resolve(configured ?? dirname(fileURLToPath(import.meta.url)));
|
|
1772
|
+
for (; ; ) {
|
|
1773
|
+
const manifestPath = resolve(cursor, "sdk-manifest.json");
|
|
1774
|
+
if (await readable(manifestPath)) {
|
|
1775
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
1776
|
+
const templates = new Map(
|
|
1777
|
+
manifest.templates.map((template) => [template.id, resolve(cursor, template.root)])
|
|
1778
|
+
);
|
|
1779
|
+
const defaultTemplate = manifest.templates.find((template) => template.default);
|
|
1780
|
+
if (defaultTemplate === void 0) throw new Error("sdk-default-template-missing");
|
|
1781
|
+
return {
|
|
1782
|
+
root: cursor,
|
|
1783
|
+
manifest,
|
|
1784
|
+
store: resolve(cursor, "store", "pnpm"),
|
|
1785
|
+
templates,
|
|
1786
|
+
defaultTemplate: defaultTemplate.id
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1789
|
+
const parent = dirname(cursor);
|
|
1790
|
+
if (parent === cursor) return void 0;
|
|
1791
|
+
cursor = parent;
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
var init_sdk = __esm({
|
|
1795
|
+
"src/sdk.ts"() {
|
|
1796
|
+
}
|
|
1797
|
+
});
|
|
1798
|
+
function slash(path) {
|
|
1799
|
+
return path.split(sep).join("/");
|
|
1800
|
+
}
|
|
1801
|
+
function contained(parent, child) {
|
|
1802
|
+
const path = relative(parent, child);
|
|
1803
|
+
return path !== "" && path !== ".." && !path.startsWith(`..${sep}`) && !path.startsWith(sep);
|
|
1804
|
+
}
|
|
1805
|
+
async function pathKind(path) {
|
|
1806
|
+
try {
|
|
1807
|
+
const info = await lstat(path);
|
|
1808
|
+
if (info.isSymbolicLink()) return "symlink";
|
|
1809
|
+
if (info.isDirectory()) return "directory";
|
|
1810
|
+
return "file";
|
|
1811
|
+
} catch (cause) {
|
|
1812
|
+
if (cause !== null && typeof cause === "object" && "code" in cause && cause.code === "ENOENT") {
|
|
1813
|
+
return "missing";
|
|
1814
|
+
}
|
|
1815
|
+
throw cause;
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
async function filesUnder2(root, directory = root) {
|
|
1819
|
+
const files = [];
|
|
1820
|
+
for (const name of (await readdir(directory)).sort()) {
|
|
1821
|
+
const path = resolve(directory, name);
|
|
1822
|
+
const info = await lstat(path);
|
|
1823
|
+
if (info.isSymbolicLink()) throw new Error(`skill-source-symlink: ${path}`);
|
|
1824
|
+
if (info.isDirectory()) files.push(...await filesUnder2(root, path));
|
|
1825
|
+
else if (info.isFile()) files.push(path);
|
|
1826
|
+
}
|
|
1827
|
+
return files;
|
|
1828
|
+
}
|
|
1829
|
+
async function skillRow(sourceRoot, id) {
|
|
1830
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(id)) throw new Error(`skill-id-invalid: ${id}`);
|
|
1831
|
+
const root = resolve(sourceRoot, id);
|
|
1832
|
+
if (await pathKind(root) !== "directory") throw new Error(`skill-source-invalid: ${id}`);
|
|
1833
|
+
if (await pathKind(resolve(root, "SKILL.md")) !== "file") {
|
|
1834
|
+
throw new Error(`skill-entry-missing: ${id}/SKILL.md`);
|
|
1835
|
+
}
|
|
1836
|
+
const files = await filesUnder2(root);
|
|
1837
|
+
return {
|
|
1838
|
+
id,
|
|
1839
|
+
root: `skills/${id}`,
|
|
1840
|
+
fileCount: files.length,
|
|
1841
|
+
byteCount: (await Promise.all(files.map(async (path) => (await readFile(path)).byteLength))).reduce((sum, bytes) => sum + bytes, 0)
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
async function discoverProjectSkills(root) {
|
|
1845
|
+
const sourceRoot = resolve(root, "skills");
|
|
1846
|
+
if (await pathKind(sourceRoot) !== "directory") throw new Error("project-skills-missing");
|
|
1847
|
+
const entries2 = await readdir(sourceRoot, { withFileTypes: true });
|
|
1848
|
+
const ids = entries2.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
1849
|
+
if (ids.length === 0) throw new Error("project-skills-empty");
|
|
1850
|
+
return Promise.all(ids.map((id) => skillRow(sourceRoot, id)));
|
|
1851
|
+
}
|
|
1852
|
+
async function sameFiles(leftRoot, rightRoot) {
|
|
1853
|
+
const left = await filesUnder2(leftRoot);
|
|
1854
|
+
const right = await filesUnder2(rightRoot);
|
|
1855
|
+
const leftNames = left.map((path) => slash(relative(leftRoot, path)));
|
|
1856
|
+
const rightNames = right.map((path) => slash(relative(rightRoot, path)));
|
|
1857
|
+
if (JSON.stringify(leftNames) !== JSON.stringify(rightNames)) return false;
|
|
1858
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
1859
|
+
const leftPath = left[index];
|
|
1860
|
+
const rightPath = right[index];
|
|
1861
|
+
if (leftPath === void 0 || rightPath === void 0) return false;
|
|
1862
|
+
if (!(await readFile(leftPath)).equals(await readFile(rightPath))) return false;
|
|
1863
|
+
}
|
|
1864
|
+
return true;
|
|
1865
|
+
}
|
|
1866
|
+
async function copySdkSkills(sdk, projectRoot2) {
|
|
1867
|
+
const targetSourceRoot = resolve(projectRoot2, "skills");
|
|
1868
|
+
const copies = [];
|
|
1869
|
+
for (const skill of sdk.manifest.skills) {
|
|
1870
|
+
const source = resolve(sdk.root, skill.root);
|
|
1871
|
+
const destination = resolve(projectRoot2, skill.root);
|
|
1872
|
+
if (!contained(sdk.root, source) || !contained(projectRoot2, destination)) {
|
|
1873
|
+
throw new Error(`sdk-skill-path-invalid: ${skill.id}`);
|
|
1874
|
+
}
|
|
1875
|
+
const actual = await skillRow(resolve(sdk.root, "skills"), skill.id);
|
|
1876
|
+
if (actual.root !== skill.root || actual.fileCount !== skill.fileCount || actual.byteCount !== skill.byteCount) {
|
|
1877
|
+
throw new Error(`sdk-skill-manifest-drift: ${skill.id}`);
|
|
1878
|
+
}
|
|
1879
|
+
const kind = await pathKind(destination);
|
|
1880
|
+
if (kind === "missing") {
|
|
1881
|
+
copies.push({ source, destination });
|
|
1882
|
+
} else if (kind !== "directory" || !await sameFiles(source, destination)) {
|
|
1883
|
+
throw new Error(`project-skill-source-conflict: ${skill.id}`);
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
const created = [];
|
|
1887
|
+
try {
|
|
1888
|
+
await mkdir(targetSourceRoot, { recursive: true });
|
|
1889
|
+
for (const { source, destination } of copies) {
|
|
1890
|
+
await cp(source, destination, { recursive: true, errorOnExist: true, force: false });
|
|
1891
|
+
created.push(destination);
|
|
1892
|
+
}
|
|
1893
|
+
} catch (cause) {
|
|
1894
|
+
await Promise.all(created.map((path) => rm(path, { recursive: true, force: true })));
|
|
1895
|
+
throw cause;
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
function expectedLinkTarget(projectRoot2, mountRoot, id) {
|
|
1899
|
+
const mountParent = resolve(projectRoot2, mountRoot);
|
|
1900
|
+
const target = resolve(projectRoot2, "skills", id);
|
|
1901
|
+
return process.platform === "win32" ? target : relative(mountParent, target);
|
|
1902
|
+
}
|
|
1903
|
+
async function linkMatches(path, target) {
|
|
1904
|
+
if (await pathKind(path) !== "symlink") return false;
|
|
1905
|
+
const actual = await readlink(path);
|
|
1906
|
+
return resolve(dirname(path), actual) === resolve(dirname(path), target);
|
|
1907
|
+
}
|
|
1908
|
+
function updateManagedGitignore(previous, skillIds) {
|
|
1909
|
+
const block = `${GITIGNORE_BEGIN}
|
|
1910
|
+
${skillIds.map((id) => `/${id}`).join("\n")}
|
|
1911
|
+
${GITIGNORE_END}`;
|
|
1912
|
+
const begin = previous.indexOf(GITIGNORE_BEGIN);
|
|
1913
|
+
const end = previous.indexOf(GITIGNORE_END);
|
|
1914
|
+
if (begin >= 0 && end >= begin) {
|
|
1915
|
+
const after = end + GITIGNORE_END.length;
|
|
1916
|
+
return `${previous.slice(0, begin)}${block}${previous.slice(after)}`.replace(/^\n+/, "");
|
|
1917
|
+
}
|
|
1918
|
+
return previous.length === 0 ? `${block}
|
|
1919
|
+
` : `${previous.replace(/\s*$/, "")}
|
|
1920
|
+
|
|
1921
|
+
${block}
|
|
1922
|
+
`;
|
|
1923
|
+
}
|
|
1924
|
+
function removeManagedGitignore(previous) {
|
|
1925
|
+
const begin = previous.indexOf(GITIGNORE_BEGIN);
|
|
1926
|
+
const end = previous.indexOf(GITIGNORE_END);
|
|
1927
|
+
if (begin < 0 || end < begin) return previous;
|
|
1928
|
+
const before = previous.slice(0, begin).trimEnd();
|
|
1929
|
+
const after = previous.slice(end + GITIGNORE_END.length).trimStart();
|
|
1930
|
+
const retained = [before, after].filter((part) => part.length > 0).join("\n");
|
|
1931
|
+
return retained.length === 0 ? "" : `${retained}
|
|
1932
|
+
`;
|
|
1933
|
+
}
|
|
1934
|
+
async function removeEmptyDirectory(path) {
|
|
1935
|
+
try {
|
|
1936
|
+
await rmdir(path);
|
|
1937
|
+
} catch (cause) {
|
|
1938
|
+
if (cause !== null && typeof cause === "object" && "code" in cause && (cause.code === "ENOENT" || cause.code === "ENOTEMPTY" || cause.code === "EEXIST")) {
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
throw cause;
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
async function readOptional(path) {
|
|
1945
|
+
try {
|
|
1946
|
+
return await readFile(path, "utf8");
|
|
1947
|
+
} catch (cause) {
|
|
1948
|
+
if (cause !== null && typeof cause === "object" && "code" in cause && cause.code === "ENOENT") {
|
|
1949
|
+
return void 0;
|
|
1950
|
+
}
|
|
1951
|
+
throw cause;
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
async function readInstallManifest(root) {
|
|
1955
|
+
const value = await readOptional(resolve(root, MANIFEST_PATH));
|
|
1956
|
+
return value === void 0 ? void 0 : JSON.parse(value);
|
|
1957
|
+
}
|
|
1958
|
+
async function installProjectSkills(root, provenance) {
|
|
1959
|
+
const projectRoot2 = resolve(root);
|
|
1960
|
+
const skills = await discoverProjectSkills(projectRoot2);
|
|
1961
|
+
const ids = skills.map((skill) => skill.id);
|
|
1962
|
+
const priorManifest = await readInstallManifest(projectRoot2);
|
|
1963
|
+
const desiredLinks = PROJECT_SKILL_MOUNT_ROOTS.flatMap(
|
|
1964
|
+
(mountRoot) => ids.map((id) => ({
|
|
1965
|
+
path: resolve(projectRoot2, mountRoot, id),
|
|
1966
|
+
target: expectedLinkTarget(projectRoot2, mountRoot, id)
|
|
1967
|
+
}))
|
|
1968
|
+
);
|
|
1969
|
+
const desiredPaths = new Set(desiredLinks.map((entry) => entry.path));
|
|
1970
|
+
const desiredMountRoots = new Set(PROJECT_SKILL_MOUNT_ROOTS);
|
|
1971
|
+
const retiredMountRoots = (priorManifest?.mounts ?? []).map((mount) => mount.root).filter(
|
|
1972
|
+
(mountRoot) => !desiredMountRoots.has(mountRoot) && RETIRED_PROJECT_SKILL_MOUNT_ROOTS.has(mountRoot)
|
|
1973
|
+
);
|
|
1974
|
+
const staleLinks = (priorManifest?.mounts ?? []).flatMap(
|
|
1975
|
+
(mount) => mount.skills.map((id) => ({
|
|
1976
|
+
path: resolve(projectRoot2, mount.root, id),
|
|
1977
|
+
target: expectedLinkTarget(projectRoot2, mount.root, id)
|
|
1978
|
+
}))
|
|
1979
|
+
).filter((entry) => !desiredPaths.has(entry.path));
|
|
1980
|
+
for (const entry of desiredLinks) {
|
|
1981
|
+
const kind = await pathKind(entry.path);
|
|
1982
|
+
if (kind !== "missing" && !await linkMatches(entry.path, entry.target)) {
|
|
1983
|
+
throw new Error(`skill-mount-conflict: ${slash(relative(projectRoot2, entry.path))}`);
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
for (const entry of staleLinks) {
|
|
1987
|
+
const kind = await pathKind(entry.path);
|
|
1988
|
+
if (kind !== "missing" && !await linkMatches(entry.path, entry.target)) {
|
|
1989
|
+
throw new Error(`skill-stale-mount-conflict: ${slash(relative(projectRoot2, entry.path))}`);
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
const ignoreSnapshots = /* @__PURE__ */ new Map();
|
|
1993
|
+
const createdLinks = [];
|
|
1994
|
+
const removedLinks = [];
|
|
1995
|
+
const manifestPath = resolve(projectRoot2, MANIFEST_PATH);
|
|
1996
|
+
const priorManifestText = await readOptional(manifestPath);
|
|
1997
|
+
try {
|
|
1998
|
+
for (const entry of staleLinks) {
|
|
1999
|
+
if (await pathKind(entry.path) === "symlink") {
|
|
2000
|
+
await rm(entry.path);
|
|
2001
|
+
removedLinks.push(entry);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
for (const mountRoot of retiredMountRoots) {
|
|
2005
|
+
const mountParent = resolve(projectRoot2, mountRoot);
|
|
2006
|
+
const ignorePath = resolve(mountParent, ".gitignore");
|
|
2007
|
+
const previous = await readOptional(ignorePath);
|
|
2008
|
+
ignoreSnapshots.set(ignorePath, previous);
|
|
2009
|
+
if (previous !== void 0) {
|
|
2010
|
+
const retained = removeManagedGitignore(previous);
|
|
2011
|
+
if (retained.length === 0) await rm(ignorePath);
|
|
2012
|
+
else await writeFile(ignorePath, retained);
|
|
2013
|
+
}
|
|
2014
|
+
await removeEmptyDirectory(mountParent);
|
|
2015
|
+
await removeEmptyDirectory(dirname(mountParent));
|
|
2016
|
+
}
|
|
2017
|
+
for (const mountRoot of PROJECT_SKILL_MOUNT_ROOTS) {
|
|
2018
|
+
const mountParent = resolve(projectRoot2, mountRoot);
|
|
2019
|
+
await mkdir(mountParent, { recursive: true });
|
|
2020
|
+
const ignorePath = resolve(mountParent, ".gitignore");
|
|
2021
|
+
const previous = await readOptional(ignorePath);
|
|
2022
|
+
ignoreSnapshots.set(ignorePath, previous);
|
|
2023
|
+
await writeFile(ignorePath, updateManagedGitignore(previous ?? "", ids));
|
|
2024
|
+
for (const id of ids) {
|
|
2025
|
+
const path = resolve(mountParent, id);
|
|
2026
|
+
if (await pathKind(path) === "missing") {
|
|
2027
|
+
const target = expectedLinkTarget(projectRoot2, mountRoot, id);
|
|
2028
|
+
await symlink(target, path, process.platform === "win32" ? "junction" : "dir");
|
|
2029
|
+
createdLinks.push(path);
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
const sdkVersion = provenance?.sdkVersion ?? priorManifest?.sdkVersion;
|
|
2034
|
+
const engineCommit = provenance?.engineCommit ?? priorManifest?.engineCommit;
|
|
2035
|
+
const manifest = {
|
|
2036
|
+
schemaVersion: "1.0.0",
|
|
2037
|
+
sourceRoot: "skills",
|
|
2038
|
+
...sdkVersion === void 0 ? {} : { sdkVersion },
|
|
2039
|
+
...engineCommit === void 0 ? {} : { engineCommit },
|
|
2040
|
+
skills,
|
|
2041
|
+
mounts: PROJECT_SKILL_MOUNT_ROOTS.map((mountRoot) => ({ root: mountRoot, skills: ids }))
|
|
2042
|
+
};
|
|
2043
|
+
await mkdir(dirname(manifestPath), { recursive: true });
|
|
2044
|
+
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
2045
|
+
`);
|
|
2046
|
+
} catch (cause) {
|
|
2047
|
+
await Promise.all(createdLinks.map((path) => rm(path, { force: true })));
|
|
2048
|
+
await Promise.all(
|
|
2049
|
+
removedLinks.map(async ({ path, target }) => {
|
|
2050
|
+
await mkdir(dirname(path), { recursive: true });
|
|
2051
|
+
await symlink(target, path, process.platform === "win32" ? "junction" : "dir");
|
|
2052
|
+
})
|
|
2053
|
+
);
|
|
2054
|
+
await Promise.all(
|
|
2055
|
+
[...ignoreSnapshots].map(async ([path, value]) => {
|
|
2056
|
+
if (value === void 0) await rm(path, { force: true });
|
|
2057
|
+
else {
|
|
2058
|
+
await mkdir(dirname(path), { recursive: true });
|
|
2059
|
+
await writeFile(path, value);
|
|
2060
|
+
}
|
|
2061
|
+
})
|
|
2062
|
+
);
|
|
2063
|
+
if (priorManifestText === void 0) await rm(manifestPath, { force: true });
|
|
2064
|
+
else await writeFile(manifestPath, priorManifestText);
|
|
2065
|
+
throw cause;
|
|
2066
|
+
}
|
|
2067
|
+
return {
|
|
2068
|
+
root: projectRoot2,
|
|
2069
|
+
sourceRoot: resolve(projectRoot2, "skills"),
|
|
2070
|
+
manifest: manifestPath,
|
|
2071
|
+
skills: ids,
|
|
2072
|
+
mountRoots: PROJECT_SKILL_MOUNT_ROOTS
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
2075
|
+
async function verifyProjectSkills(root) {
|
|
2076
|
+
const projectRoot2 = resolve(root);
|
|
2077
|
+
const skills = await discoverProjectSkills(projectRoot2);
|
|
2078
|
+
const ids = skills.map((skill) => skill.id);
|
|
2079
|
+
const manifest = await readInstallManifest(projectRoot2);
|
|
2080
|
+
if (manifest === void 0) throw new Error("skill-install-manifest-missing");
|
|
2081
|
+
if (manifest.schemaVersion !== "1.0.0" || manifest.sourceRoot !== "skills" || JSON.stringify(manifest.skills) !== JSON.stringify(skills) || JSON.stringify(manifest.mounts.map((mount) => mount.root)) !== JSON.stringify(PROJECT_SKILL_MOUNT_ROOTS)) {
|
|
2082
|
+
throw new Error("skill-install-manifest-drift");
|
|
2083
|
+
}
|
|
2084
|
+
for (const mountRoot of PROJECT_SKILL_MOUNT_ROOTS) {
|
|
2085
|
+
const declared = manifest.mounts.find((mount) => mount.root === mountRoot);
|
|
2086
|
+
if (declared === void 0 || JSON.stringify(declared.skills) !== JSON.stringify(ids)) {
|
|
2087
|
+
throw new Error(`skill-install-manifest-mount-drift: ${mountRoot}`);
|
|
2088
|
+
}
|
|
2089
|
+
const ignore = await readOptional(resolve(projectRoot2, mountRoot, ".gitignore"));
|
|
2090
|
+
if (ignore === void 0 || updateManagedGitignore(ignore, ids) !== ignore) {
|
|
2091
|
+
throw new Error(`skill-mount-gitignore-drift: ${mountRoot}`);
|
|
2092
|
+
}
|
|
2093
|
+
for (const id of ids) {
|
|
2094
|
+
const path = resolve(projectRoot2, mountRoot, id);
|
|
2095
|
+
const target = expectedLinkTarget(projectRoot2, mountRoot, id);
|
|
2096
|
+
if (!await linkMatches(path, target)) {
|
|
2097
|
+
throw new Error(`skill-mount-drift: ${mountRoot}/${id}`);
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
return {
|
|
2102
|
+
root: projectRoot2,
|
|
2103
|
+
sourceRoot: resolve(projectRoot2, "skills"),
|
|
2104
|
+
manifest: resolve(projectRoot2, MANIFEST_PATH),
|
|
2105
|
+
skills: ids,
|
|
2106
|
+
mountRoots: PROJECT_SKILL_MOUNT_ROOTS
|
|
2107
|
+
};
|
|
2108
|
+
}
|
|
2109
|
+
async function skillInstallCommand(options = {}) {
|
|
2110
|
+
const facts = await readProjectFacts(options.root);
|
|
2111
|
+
if (!facts.ok) return facts;
|
|
2112
|
+
try {
|
|
2113
|
+
return { ok: true, value: await installProjectSkills(facts.value.root) };
|
|
2114
|
+
} catch (cause) {
|
|
2115
|
+
return { ok: false, error: commandError(cause, "skill-install-failed") };
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
async function skillVerifyCommand(options = {}) {
|
|
2119
|
+
const facts = await readProjectFacts(options.root);
|
|
2120
|
+
if (!facts.ok) return facts;
|
|
2121
|
+
try {
|
|
2122
|
+
return { ok: true, value: await verifyProjectSkills(facts.value.root) };
|
|
2123
|
+
} catch (cause) {
|
|
2124
|
+
return { ok: false, error: commandError(cause, "skill-verify-failed") };
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
var PROJECT_SKILL_MOUNT_ROOTS, RETIRED_PROJECT_SKILL_MOUNT_ROOTS, MANIFEST_PATH, GITIGNORE_BEGIN, GITIGNORE_END;
|
|
2128
|
+
var init_skill_install = __esm({
|
|
2129
|
+
"src/skill-install.ts"() {
|
|
2130
|
+
init_project();
|
|
2131
|
+
PROJECT_SKILL_MOUNT_ROOTS = Object.freeze([
|
|
2132
|
+
".codebuddy/skills",
|
|
2133
|
+
".cursor/skills",
|
|
2134
|
+
".agents/skills",
|
|
2135
|
+
".claude/skills",
|
|
2136
|
+
".workbuddy/skills",
|
|
2137
|
+
".forgeax/skills"
|
|
2138
|
+
]);
|
|
2139
|
+
RETIRED_PROJECT_SKILL_MOUNT_ROOTS = /* @__PURE__ */ new Set([".claude-internal/skills"]);
|
|
2140
|
+
MANIFEST_PATH = ".forgeax/skill-install-manifest.json";
|
|
2141
|
+
GITIGNORE_BEGIN = "# BEGIN FORGEAX MANAGED SKILLS";
|
|
2142
|
+
GITIGNORE_END = "# END FORGEAX MANAGED SKILLS";
|
|
2143
|
+
}
|
|
2144
|
+
});
|
|
2145
|
+
function isMissingPathError(cause) {
|
|
2146
|
+
return cause !== null && typeof cause === "object" && "code" in cause && cause.code === "ENOENT";
|
|
2147
|
+
}
|
|
2148
|
+
async function canonicalProspectivePath(path) {
|
|
2149
|
+
let cursor = resolve(path);
|
|
2150
|
+
const suffix = [];
|
|
2151
|
+
for (; ; ) {
|
|
2152
|
+
try {
|
|
2153
|
+
return resolve(await realpath(cursor), ...suffix.reverse());
|
|
2154
|
+
} catch (cause) {
|
|
2155
|
+
if (!isMissingPathError(cause)) throw cause;
|
|
2156
|
+
const parent = dirname(cursor);
|
|
2157
|
+
if (parent === cursor) throw cause;
|
|
2158
|
+
suffix.push(basename(cursor));
|
|
2159
|
+
cursor = parent;
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
function containsOrEquals(parent, child) {
|
|
2164
|
+
const path = relative(parent, child);
|
|
2165
|
+
return path === "" || path !== ".." && !path.startsWith(`..${sep}`) && !isAbsolute(path);
|
|
2166
|
+
}
|
|
2167
|
+
function nodeSupported() {
|
|
2168
|
+
const [major = 0, minor = 0] = process.versions.node.split(".").map(Number);
|
|
2169
|
+
return major > 22 || major === 22 && minor >= 13;
|
|
2170
|
+
}
|
|
2171
|
+
function pnpmSupported(version) {
|
|
2172
|
+
const [major = 0, minor = 0] = version.split(".").map(Number);
|
|
2173
|
+
return major === 10 && minor >= 33;
|
|
2174
|
+
}
|
|
2175
|
+
async function doctorCommand(options = {}) {
|
|
2176
|
+
const facts = await readProjectFacts(options.root);
|
|
2177
|
+
if (!facts.ok) return facts;
|
|
2178
|
+
let pnpm;
|
|
2179
|
+
try {
|
|
2180
|
+
pnpm = (await execFileAsync("pnpm", ["--version"], { cwd: facts.value.root })).stdout.trim();
|
|
2181
|
+
} catch (cause) {
|
|
2182
|
+
return {
|
|
2183
|
+
ok: false,
|
|
2184
|
+
error: {
|
|
2185
|
+
code: "pnpm-unavailable",
|
|
2186
|
+
expected: "pnpm to be available on PATH",
|
|
2187
|
+
hint: "Install the SDK-supported pnpm version and retry.",
|
|
2188
|
+
detail: { reason: cause instanceof Error ? cause.message : String(cause) }
|
|
2189
|
+
}
|
|
2190
|
+
};
|
|
2191
|
+
}
|
|
2192
|
+
if (!nodeSupported()) {
|
|
2193
|
+
return {
|
|
2194
|
+
ok: false,
|
|
2195
|
+
error: {
|
|
2196
|
+
code: "node-version-unsupported",
|
|
2197
|
+
expected: "Node.js >=22.13.0",
|
|
2198
|
+
hint: "Select a supported Node.js installation and retry.",
|
|
2199
|
+
detail: { actual: process.versions.node }
|
|
2200
|
+
}
|
|
2201
|
+
};
|
|
2202
|
+
}
|
|
2203
|
+
if (!pnpmSupported(pnpm)) {
|
|
2204
|
+
return {
|
|
2205
|
+
ok: false,
|
|
2206
|
+
error: {
|
|
2207
|
+
code: "pnpm-version-unsupported",
|
|
2208
|
+
expected: "pnpm >=10.33.0 <11",
|
|
2209
|
+
hint: "Enable the packageManager-declared pnpm version with Corepack and retry.",
|
|
2210
|
+
detail: { actual: pnpm }
|
|
2211
|
+
}
|
|
2212
|
+
};
|
|
2213
|
+
}
|
|
2214
|
+
const workspaceDependencies = [];
|
|
2215
|
+
for (const section of ["dependencies", "devDependencies"]) {
|
|
2216
|
+
const value = facts.value.packageJson[section];
|
|
2217
|
+
if (value === null || typeof value !== "object") continue;
|
|
2218
|
+
for (const [name, version] of Object.entries(value)) {
|
|
2219
|
+
if (typeof version === "string" && (version.startsWith("workspace:") || version.startsWith("file:"))) {
|
|
2220
|
+
workspaceDependencies.push(name);
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
if (workspaceDependencies.length > 0) {
|
|
2225
|
+
return {
|
|
2226
|
+
ok: false,
|
|
2227
|
+
error: {
|
|
2228
|
+
code: "project-local-dependency",
|
|
2229
|
+
expected: "all external project dependencies to use SDK-resolved exact versions",
|
|
2230
|
+
hint: "Run forgeax init from the unpacked SDK and commit the resulting lockfile.",
|
|
2231
|
+
detail: { dependencies: workspaceDependencies.sort() }
|
|
2232
|
+
}
|
|
2233
|
+
};
|
|
2234
|
+
}
|
|
2235
|
+
return {
|
|
2236
|
+
ok: true,
|
|
2237
|
+
value: {
|
|
2238
|
+
root: facts.value.root,
|
|
2239
|
+
projectId: facts.value.id,
|
|
2240
|
+
node: process.versions.node,
|
|
2241
|
+
pnpm,
|
|
2242
|
+
workspaceDependencies
|
|
2243
|
+
}
|
|
2244
|
+
};
|
|
2245
|
+
}
|
|
2246
|
+
async function initCommand(options = {}) {
|
|
2247
|
+
const facts = await readProjectFacts(options.root);
|
|
2248
|
+
if (!facts.ok) return facts;
|
|
2249
|
+
try {
|
|
2250
|
+
const sdk = await findSdkContext();
|
|
2251
|
+
const plan = createInitPlan(facts.value, sdk?.manifest);
|
|
2252
|
+
if (!plan.ok) return plan;
|
|
2253
|
+
const applied = await applyInitPlan(facts.value, plan.value, options);
|
|
2254
|
+
if (!applied.ok || options.dryRun === true) return applied;
|
|
2255
|
+
if (sdk !== void 0) {
|
|
2256
|
+
const template = sdk.templates.get(sdk.defaultTemplate);
|
|
2257
|
+
if (template === void 0) throw new Error("sdk-default-template-missing");
|
|
2258
|
+
await copyFile(
|
|
2259
|
+
resolve(template, "pnpm-lock.yaml"),
|
|
2260
|
+
resolve(facts.value.root, "pnpm-lock.yaml")
|
|
2261
|
+
);
|
|
2262
|
+
await copySdkSkills(sdk, facts.value.root);
|
|
2263
|
+
await installProjectSkills(facts.value.root, sdk.manifest);
|
|
2264
|
+
}
|
|
2265
|
+
if (options.install === false) return applied;
|
|
2266
|
+
const installArgs = sdk === void 0 ? ["install", "--frozen-lockfile=false"] : [
|
|
2267
|
+
"install",
|
|
2268
|
+
"--offline",
|
|
2269
|
+
"--frozen-lockfile",
|
|
2270
|
+
"--side-effects-cache=false",
|
|
2271
|
+
"--store-dir",
|
|
2272
|
+
sdk.store
|
|
2273
|
+
];
|
|
2274
|
+
await execFileAsync("pnpm", installArgs, {
|
|
2275
|
+
cwd: facts.value.root,
|
|
2276
|
+
env: { ...process.env, CI: "true" },
|
|
2277
|
+
maxBuffer: 16 * 1024 * 1024
|
|
2278
|
+
});
|
|
2279
|
+
return applied;
|
|
2280
|
+
} catch (cause) {
|
|
2281
|
+
return { ok: false, error: commandError(cause, "project-init-failed") };
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
async function newCommand(options = {}) {
|
|
2285
|
+
try {
|
|
2286
|
+
const sdk = await findSdkContext();
|
|
2287
|
+
if (sdk === void 0) {
|
|
2288
|
+
return {
|
|
2289
|
+
ok: false,
|
|
2290
|
+
error: {
|
|
2291
|
+
code: "sdk-context-missing",
|
|
2292
|
+
expected: "forgeax new to run from an unpacked ForgeaX SDK",
|
|
2293
|
+
hint: "Run the SDK archive bin/forgeax.mjs entry or set FORGEAX_SDK_ROOT.",
|
|
2294
|
+
detail: {}
|
|
2295
|
+
}
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
const root = resolve(options.root ?? process.cwd());
|
|
2299
|
+
if (containsOrEquals(
|
|
2300
|
+
await canonicalProspectivePath(sdk.root),
|
|
2301
|
+
await canonicalProspectivePath(root)
|
|
2302
|
+
)) {
|
|
2303
|
+
return {
|
|
2304
|
+
ok: false,
|
|
2305
|
+
error: {
|
|
2306
|
+
code: "project-target-inside-sdk",
|
|
2307
|
+
expected: "forgeax new target to be outside the unpacked SDK root",
|
|
2308
|
+
hint: "Choose a sibling directory or an absolute path outside the SDK.",
|
|
2309
|
+
detail: { root, sdkRoot: sdk.root }
|
|
2310
|
+
}
|
|
2311
|
+
};
|
|
2312
|
+
}
|
|
2313
|
+
const templateId = options.template ?? sdk.defaultTemplate;
|
|
2314
|
+
const template = sdk.templates.get(templateId);
|
|
2315
|
+
if (template === void 0) {
|
|
2316
|
+
return {
|
|
2317
|
+
ok: false,
|
|
2318
|
+
error: {
|
|
2319
|
+
code: "sdk-template-not-found",
|
|
2320
|
+
expected: "a template id declared by sdk-manifest.json",
|
|
2321
|
+
hint: `Choose one of: ${[...sdk.templates.keys()].sort().join(", ")}.`,
|
|
2322
|
+
detail: { template: templateId }
|
|
2323
|
+
}
|
|
2324
|
+
};
|
|
2325
|
+
}
|
|
2326
|
+
let targetExists = true;
|
|
2327
|
+
let entries2;
|
|
2328
|
+
try {
|
|
2329
|
+
entries2 = await readdir(root);
|
|
2330
|
+
} catch (cause) {
|
|
2331
|
+
if (!isMissingPathError(cause)) throw cause;
|
|
2332
|
+
targetExists = false;
|
|
2333
|
+
entries2 = [];
|
|
2334
|
+
}
|
|
2335
|
+
if (entries2.length > 0) {
|
|
2336
|
+
return {
|
|
2337
|
+
ok: false,
|
|
2338
|
+
error: {
|
|
2339
|
+
code: "project-target-not-empty",
|
|
2340
|
+
expected: "forgeax new target to be absent or empty",
|
|
2341
|
+
hint: "Choose an empty directory so existing files cannot be overwritten.",
|
|
2342
|
+
detail: { root }
|
|
2343
|
+
}
|
|
2344
|
+
};
|
|
2345
|
+
}
|
|
2346
|
+
if (options.dryRun === true) {
|
|
2347
|
+
return {
|
|
2348
|
+
ok: true,
|
|
2349
|
+
value: { root, template: templateId, sdkVersion: sdk.manifest.sdkVersion }
|
|
2350
|
+
};
|
|
2351
|
+
}
|
|
2352
|
+
const parent = dirname(root);
|
|
2353
|
+
await mkdir(parent, { recursive: true });
|
|
2354
|
+
let staging = await mkdtemp(
|
|
2355
|
+
resolve(parent, `.${basename(root)}.forgeax-staging-`)
|
|
2356
|
+
);
|
|
2357
|
+
const committedNames = [];
|
|
2358
|
+
try {
|
|
2359
|
+
for (const name of await readdir(template)) {
|
|
2360
|
+
await cp(resolve(template, name), resolve(staging, name), {
|
|
2361
|
+
recursive: true,
|
|
2362
|
+
errorOnExist: true,
|
|
2363
|
+
force: false
|
|
2364
|
+
});
|
|
2365
|
+
}
|
|
2366
|
+
await copySdkSkills(sdk, staging);
|
|
2367
|
+
await installProjectSkills(staging, sdk.manifest);
|
|
2368
|
+
await execFileAsync(
|
|
2369
|
+
"pnpm",
|
|
2370
|
+
[
|
|
2371
|
+
"install",
|
|
2372
|
+
"--offline",
|
|
2373
|
+
"--frozen-lockfile",
|
|
2374
|
+
"--side-effects-cache=false",
|
|
2375
|
+
"--store-dir",
|
|
2376
|
+
sdk.store
|
|
2377
|
+
],
|
|
2378
|
+
{
|
|
2379
|
+
cwd: staging,
|
|
2380
|
+
env: { ...process.env, CI: "true" },
|
|
2381
|
+
maxBuffer: 16 * 1024 * 1024
|
|
2382
|
+
}
|
|
2383
|
+
);
|
|
2384
|
+
if (!targetExists) {
|
|
2385
|
+
await rename(staging, root);
|
|
2386
|
+
staging = void 0;
|
|
2387
|
+
} else {
|
|
2388
|
+
for (const name of await readdir(staging)) {
|
|
2389
|
+
await rename(resolve(staging, name), resolve(root, name));
|
|
2390
|
+
committedNames.push(name);
|
|
2391
|
+
}
|
|
2392
|
+
await rm(staging, { recursive: true, force: true });
|
|
2393
|
+
staging = void 0;
|
|
2394
|
+
}
|
|
2395
|
+
return {
|
|
2396
|
+
ok: true,
|
|
2397
|
+
value: { root, template: templateId, sdkVersion: sdk.manifest.sdkVersion }
|
|
2398
|
+
};
|
|
2399
|
+
} catch (cause) {
|
|
2400
|
+
if (staging !== void 0) await rm(staging, { recursive: true, force: true });
|
|
2401
|
+
await Promise.all(
|
|
2402
|
+
committedNames.map((name) => rm(resolve(root, name), { recursive: true, force: true }))
|
|
2403
|
+
);
|
|
2404
|
+
return { ok: false, error: commandError(cause, "project-create-failed") };
|
|
2405
|
+
}
|
|
2406
|
+
} catch (cause) {
|
|
2407
|
+
return { ok: false, error: commandError(cause, "project-create-failed") };
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
var execFileAsync;
|
|
2411
|
+
var init_bootstrap_commands = __esm({
|
|
2412
|
+
"src/bootstrap-commands.ts"() {
|
|
2413
|
+
init_init();
|
|
2414
|
+
init_project();
|
|
2415
|
+
init_sdk();
|
|
2416
|
+
init_skill_install();
|
|
2417
|
+
execFileAsync = promisify(execFile);
|
|
2418
|
+
}
|
|
2419
|
+
});
|
|
2420
|
+
|
|
2421
|
+
// src/plugin-authoring.ts
|
|
2422
|
+
var plugin_authoring_exports = {};
|
|
2423
|
+
__export(plugin_authoring_exports, {
|
|
2424
|
+
pluginInstallCommand: () => pluginInstallCommand,
|
|
2425
|
+
pluginUninstallCommand: () => pluginUninstallCommand
|
|
2426
|
+
});
|
|
2427
|
+
async function readManifest(root) {
|
|
2428
|
+
const path = resolve(root, "forge.json");
|
|
2429
|
+
try {
|
|
2430
|
+
const raw = await readFile(path, "utf8");
|
|
2431
|
+
return { ok: true, value: { raw, value: JSON.parse(raw) } };
|
|
2432
|
+
} catch (cause) {
|
|
2433
|
+
return {
|
|
2434
|
+
ok: false,
|
|
2435
|
+
error: {
|
|
2436
|
+
code: "plugin-manifest-unreadable",
|
|
2437
|
+
expected: "a readable forge.json",
|
|
2438
|
+
hint: "Repair the project manifest before changing plugin installation state.",
|
|
2439
|
+
detail: { path, reason: cause instanceof Error ? cause.message : String(cause) }
|
|
2440
|
+
}
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
async function writeManifest(root, value) {
|
|
2445
|
+
const path = resolve(root, "forge.json");
|
|
2446
|
+
const temporary = resolve(root, `.forgeax-plugin-${process.pid}.tmp`);
|
|
2447
|
+
await writeFile(temporary, `${JSON.stringify(value, null, 2)}
|
|
2448
|
+
`);
|
|
2449
|
+
try {
|
|
2450
|
+
await rename(temporary, path);
|
|
2451
|
+
} catch (cause) {
|
|
2452
|
+
await unlink(temporary).catch(() => void 0);
|
|
2453
|
+
throw cause;
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
function allEntries(entries2) {
|
|
2457
|
+
return entries2.flatMap((entry) => [
|
|
2458
|
+
entry,
|
|
2459
|
+
...entry.group === true ? allEntries(entry.config) : []
|
|
2460
|
+
]);
|
|
2461
|
+
}
|
|
2462
|
+
function withoutEntry(entries2, id) {
|
|
2463
|
+
return entries2.flatMap((entry) => {
|
|
2464
|
+
if (entry.id === id) return [];
|
|
2465
|
+
if (entry.group !== true) return [entry];
|
|
2466
|
+
return [
|
|
2467
|
+
{
|
|
2468
|
+
...entry,
|
|
2469
|
+
config: withoutEntry(entry.config, id)
|
|
2470
|
+
}
|
|
2471
|
+
];
|
|
2472
|
+
});
|
|
2473
|
+
}
|
|
2474
|
+
async function mutateDependency(root, action, dependency) {
|
|
2475
|
+
if (dependency === void 0) return;
|
|
2476
|
+
await execFileAsync2("pnpm", [action, dependency], { cwd: root, maxBuffer: 16 * 1024 * 1024 });
|
|
2477
|
+
}
|
|
2478
|
+
async function pluginInstallCommand(options) {
|
|
2479
|
+
const root = resolve(options.root ?? process.cwd());
|
|
2480
|
+
const manifest = await readManifest(root);
|
|
2481
|
+
if (!manifest.ok) return manifest;
|
|
2482
|
+
const parsed = GameProjectSchema.safeParse(manifest.value.value);
|
|
2483
|
+
if (!parsed.success) {
|
|
2484
|
+
return {
|
|
2485
|
+
ok: false,
|
|
2486
|
+
error: {
|
|
2487
|
+
code: "plugin-manifest-invalid",
|
|
2488
|
+
expected: "forge.json to satisfy GameProjectSchema",
|
|
2489
|
+
hint: "Repair the manifest before installing a plugin.",
|
|
2490
|
+
detail: { issues: parsed.error.issues }
|
|
2491
|
+
}
|
|
2492
|
+
};
|
|
2493
|
+
}
|
|
2494
|
+
const entries2 = parsed.data.plugins ?? [];
|
|
2495
|
+
if (allEntries(entries2).some((entry) => entry.id === options.id)) {
|
|
2496
|
+
return {
|
|
2497
|
+
ok: false,
|
|
2498
|
+
error: {
|
|
2499
|
+
code: "plugin-entry-id-conflict",
|
|
2500
|
+
expected: "a project-unique plugin Entry id",
|
|
2501
|
+
hint: "Choose a stable id not already present in forge.json#plugins.",
|
|
2502
|
+
detail: { id: options.id }
|
|
2503
|
+
}
|
|
2504
|
+
};
|
|
2505
|
+
}
|
|
2506
|
+
const next = {
|
|
2507
|
+
...parsed.data,
|
|
2508
|
+
plugins: [
|
|
2509
|
+
...entries2,
|
|
2510
|
+
{ id: options.id, name: options.module, realm: options.realm ?? "engine" }
|
|
2511
|
+
]
|
|
2512
|
+
};
|
|
2513
|
+
if (options.dryRun === true) return { ok: true, value: { root, manifest: next } };
|
|
2514
|
+
try {
|
|
2515
|
+
await writeManifest(root, next);
|
|
2516
|
+
await mutateDependency(root, "add", options.dependency);
|
|
2517
|
+
return { ok: true, value: { root, id: options.id, module: options.module } };
|
|
2518
|
+
} catch (cause) {
|
|
2519
|
+
await writeFile(resolve(root, "forge.json"), manifest.value.raw);
|
|
2520
|
+
return {
|
|
2521
|
+
ok: false,
|
|
2522
|
+
error: {
|
|
2523
|
+
code: "plugin-install-failed",
|
|
2524
|
+
expected: "dependency and forge.json Entry mutations to commit together",
|
|
2525
|
+
hint: "Inspect the package-manager failure; the original forge.json was restored.",
|
|
2526
|
+
detail: { reason: cause instanceof Error ? cause.message : String(cause) }
|
|
2527
|
+
}
|
|
2528
|
+
};
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
async function pluginUninstallCommand(options) {
|
|
2532
|
+
const root = resolve(options.root ?? process.cwd());
|
|
2533
|
+
const manifest = await readManifest(root);
|
|
2534
|
+
if (!manifest.ok) return manifest;
|
|
2535
|
+
const parsed = GameProjectSchema.safeParse(manifest.value.value);
|
|
2536
|
+
if (!parsed.success) {
|
|
2537
|
+
return {
|
|
2538
|
+
ok: false,
|
|
2539
|
+
error: {
|
|
2540
|
+
code: "plugin-manifest-invalid",
|
|
2541
|
+
expected: "forge.json to satisfy GameProjectSchema",
|
|
2542
|
+
hint: "Repair the manifest before uninstalling a plugin.",
|
|
2543
|
+
detail: { issues: parsed.error.issues }
|
|
2544
|
+
}
|
|
2545
|
+
};
|
|
2546
|
+
}
|
|
2547
|
+
const entries2 = parsed.data.plugins ?? [];
|
|
2548
|
+
if (!allEntries(entries2).some((entry) => entry.id === options.id)) {
|
|
2549
|
+
return {
|
|
2550
|
+
ok: false,
|
|
2551
|
+
error: {
|
|
2552
|
+
code: "plugin-entry-missing",
|
|
2553
|
+
expected: "the plugin Entry id to exist",
|
|
2554
|
+
hint: "Inspect forge.json#plugins and pass an installed Entry id.",
|
|
2555
|
+
detail: { id: options.id }
|
|
2556
|
+
}
|
|
2557
|
+
};
|
|
2558
|
+
}
|
|
2559
|
+
const next = { ...parsed.data, plugins: withoutEntry(entries2, options.id) };
|
|
2560
|
+
if (options.dryRun === true) return { ok: true, value: { root, manifest: next } };
|
|
2561
|
+
try {
|
|
2562
|
+
await writeManifest(root, next);
|
|
2563
|
+
await mutateDependency(root, "remove", options.dependency);
|
|
2564
|
+
return { ok: true, value: { root, id: options.id } };
|
|
2565
|
+
} catch (cause) {
|
|
2566
|
+
await writeFile(resolve(root, "forge.json"), manifest.value.raw);
|
|
2567
|
+
return {
|
|
2568
|
+
ok: false,
|
|
2569
|
+
error: {
|
|
2570
|
+
code: "plugin-uninstall-failed",
|
|
2571
|
+
expected: "Entry and dependency removal to commit together",
|
|
2572
|
+
hint: "Inspect the package-manager failure; the original forge.json was restored.",
|
|
2573
|
+
detail: { reason: cause instanceof Error ? cause.message : String(cause) }
|
|
2574
|
+
}
|
|
2575
|
+
};
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
var execFileAsync2;
|
|
2579
|
+
var init_plugin_authoring = __esm({
|
|
2580
|
+
"src/plugin-authoring.ts"() {
|
|
2581
|
+
execFileAsync2 = promisify(execFile);
|
|
2582
|
+
}
|
|
2583
|
+
});
|
|
2584
|
+
var RhiError;
|
|
2585
|
+
var init_dist2 = __esm({
|
|
2586
|
+
"../rhi/dist/index.mjs"() {
|
|
2587
|
+
RhiError = class extends Error {
|
|
2588
|
+
code;
|
|
2589
|
+
expected;
|
|
2590
|
+
hint;
|
|
2591
|
+
detail;
|
|
2592
|
+
constructor(args2) {
|
|
2593
|
+
super(`[RhiError ${args2.code}] expected: ${args2.expected}; hint: ${args2.hint}`);
|
|
2594
|
+
this.name = "RhiError";
|
|
2595
|
+
this.code = args2.code;
|
|
2596
|
+
this.expected = args2.expected;
|
|
2597
|
+
this.hint = args2.hint;
|
|
2598
|
+
this.detail = args2.detail;
|
|
2599
|
+
}
|
|
2600
|
+
};
|
|
2601
|
+
}
|
|
2602
|
+
});
|
|
2603
|
+
function readPassLabel(desc) {
|
|
2604
|
+
if (desc && typeof desc.label === "string" && desc.label.length > 0) {
|
|
2605
|
+
return desc.label;
|
|
2606
|
+
}
|
|
2607
|
+
return "<unnamed>";
|
|
2608
|
+
}
|
|
2609
|
+
function readRecord(handle) {
|
|
2610
|
+
if (handle === null || typeof handle !== "object") return void 0;
|
|
2611
|
+
const tagged = handle;
|
|
2612
|
+
return tagged[BOOKKEEPING_KEY];
|
|
2613
|
+
}
|
|
2614
|
+
function isHandleDestroyed(handle) {
|
|
2615
|
+
if (readRecord(handle)?.destroyed === true) {
|
|
2616
|
+
return err(
|
|
2617
|
+
new RhiError({
|
|
2618
|
+
code: "destroy-after-destroy",
|
|
2619
|
+
expected: "GPU buffer/texture handle has not been destroyed yet",
|
|
2620
|
+
hint: "object already destroyed; track lifecycle in caller or check isDestroyed before re-destroy"
|
|
2621
|
+
})
|
|
2622
|
+
);
|
|
2623
|
+
}
|
|
2624
|
+
return ok(void 0);
|
|
2625
|
+
}
|
|
2626
|
+
function acquireCanvasContext(_canvas) {
|
|
2627
|
+
return ok(new RhiNullCanvasContext());
|
|
2628
|
+
}
|
|
2629
|
+
function createShaderModule(_device, _desc) {
|
|
2630
|
+
return Promise.resolve(ok({}));
|
|
2631
|
+
}
|
|
2632
|
+
function requestAdapter(_opts, _compatibleSurface) {
|
|
2633
|
+
return Promise.resolve(ok(new RhiNullAdapter()));
|
|
2634
|
+
}
|
|
2635
|
+
var RhiNullRenderPassEncoder, RhiNullComputePassEncoder, DeviceCounter, RhiNullCommandEncoder, BOOKKEEPING_KEY, Bookkeeper, nextDeviceId, RhiNullDevice, EMPTY_FEATURES, EMPTY_LIMITS, NEVER, RhiNullQueue, RhiNullAdapter, RhiNullCanvasContext, rhi;
|
|
2636
|
+
var init_dist3 = __esm({
|
|
2637
|
+
"../rhi-null/dist/index.mjs"() {
|
|
2638
|
+
init_dist2();
|
|
2639
|
+
RhiNullRenderPassEncoder = class {
|
|
2640
|
+
/** Number of draw* calls issued on this pass (AC-06 readback). */
|
|
2641
|
+
drawCount = 0;
|
|
2642
|
+
bindGroupCount = 0;
|
|
2643
|
+
/** Most recent setVertexBuffer / setBindGroup ownership validation; ok unless
|
|
2644
|
+
* a foreign handle was passed (AC-09 readback). */
|
|
2645
|
+
lastValidation = ok(void 0);
|
|
2646
|
+
bookkeeper;
|
|
2647
|
+
counter;
|
|
2648
|
+
passName;
|
|
2649
|
+
constructor(bookkeeper, counter, passName) {
|
|
2650
|
+
this.bookkeeper = bookkeeper;
|
|
2651
|
+
this.counter = counter;
|
|
2652
|
+
this.passName = passName;
|
|
2653
|
+
}
|
|
2654
|
+
setPipeline(_pipeline) {
|
|
2655
|
+
}
|
|
2656
|
+
setVertexBuffer(_slot, buffer, _offset, _size) {
|
|
2657
|
+
this.lastValidation = this.bookkeeper.validateOwnership(buffer);
|
|
2658
|
+
}
|
|
2659
|
+
setIndexBuffer(_buffer, _format, _offset, _size) {
|
|
2660
|
+
}
|
|
2661
|
+
setBindGroup(_index, bindGroup, _dynamicOffsetsData, _dynamicOffsetsDataStart, _dynamicOffsetsDataLength) {
|
|
2662
|
+
this.bindGroupCount++;
|
|
2663
|
+
this.counter?.recordBindGroup();
|
|
2664
|
+
this.lastValidation = this.bookkeeper.validateOwnership(bindGroup);
|
|
2665
|
+
}
|
|
2666
|
+
draw(_vertexCount, _instanceCount, _firstVertex, _firstInstance) {
|
|
2667
|
+
this.drawCount++;
|
|
2668
|
+
this.counter?.recordDraw();
|
|
2669
|
+
}
|
|
2670
|
+
drawIndexed(_indexCount, _instanceCount, _firstIndex, _baseVertex, _firstInstance) {
|
|
2671
|
+
this.drawCount++;
|
|
2672
|
+
this.counter?.recordDraw();
|
|
2673
|
+
}
|
|
2674
|
+
end() {
|
|
2675
|
+
this.counter?.recordPassName(this.passName);
|
|
2676
|
+
}
|
|
2677
|
+
setViewport(_x, _y, _w, _h, _minDepth, _maxDepth) {
|
|
2678
|
+
}
|
|
2679
|
+
setScissorRect(_x, _y, _w, _h) {
|
|
2680
|
+
}
|
|
2681
|
+
setBlendConstant(_color) {
|
|
2682
|
+
}
|
|
2683
|
+
setStencilReference(_reference) {
|
|
2684
|
+
}
|
|
2685
|
+
drawIndirect(_indirectBuffer, _indirectOffset) {
|
|
2686
|
+
this.drawCount++;
|
|
2687
|
+
this.counter?.recordDraw();
|
|
2688
|
+
}
|
|
2689
|
+
drawIndexedIndirect(_indirectBuffer, _indirectOffset) {
|
|
2690
|
+
this.drawCount++;
|
|
2691
|
+
this.counter?.recordDraw();
|
|
2692
|
+
}
|
|
2693
|
+
pushDebugGroup(_groupLabel) {
|
|
2694
|
+
}
|
|
2695
|
+
popDebugGroup() {
|
|
2696
|
+
}
|
|
2697
|
+
insertDebugMarker(_markerLabel) {
|
|
2698
|
+
}
|
|
2699
|
+
executeBundles(_bundles) {
|
|
2700
|
+
return ok(void 0);
|
|
2701
|
+
}
|
|
2702
|
+
beginOcclusionQuery(_queryIndex) {
|
|
2703
|
+
return ok(void 0);
|
|
2704
|
+
}
|
|
2705
|
+
endOcclusionQuery() {
|
|
2706
|
+
return ok(void 0);
|
|
2707
|
+
}
|
|
2708
|
+
};
|
|
2709
|
+
RhiNullComputePassEncoder = class {
|
|
2710
|
+
/** Number of dispatchWorkgroups calls issued on this pass (readback). */
|
|
2711
|
+
dispatchCount = 0;
|
|
2712
|
+
/** Most recent setBindGroup ownership validation (AC-09 readback). */
|
|
2713
|
+
lastValidation = ok(void 0);
|
|
2714
|
+
bookkeeper;
|
|
2715
|
+
counter;
|
|
2716
|
+
passName;
|
|
2717
|
+
constructor(bookkeeper, counter, passName) {
|
|
2718
|
+
this.bookkeeper = bookkeeper;
|
|
2719
|
+
this.counter = counter;
|
|
2720
|
+
this.passName = passName;
|
|
2721
|
+
}
|
|
2722
|
+
setPipeline(_pipeline) {
|
|
2723
|
+
}
|
|
2724
|
+
setBindGroup(_index, bindGroup, _dynamicOffsets) {
|
|
2725
|
+
this.lastValidation = this.bookkeeper.validateOwnership(bindGroup);
|
|
2726
|
+
}
|
|
2727
|
+
dispatchWorkgroups(_x, _y, _z) {
|
|
2728
|
+
this.dispatchCount++;
|
|
2729
|
+
this.counter?.recordDispatch();
|
|
2730
|
+
}
|
|
2731
|
+
dispatchWorkgroupsIndirect(indirectBuffer, _indirectOffset) {
|
|
2732
|
+
this.lastValidation = this.bookkeeper.validateOwnership(indirectBuffer);
|
|
2733
|
+
this.dispatchCount++;
|
|
2734
|
+
this.counter?.recordDispatch();
|
|
2735
|
+
}
|
|
2736
|
+
end() {
|
|
2737
|
+
this.counter?.recordPassName(this.passName);
|
|
2738
|
+
}
|
|
2739
|
+
};
|
|
2740
|
+
DeviceCounter = class {
|
|
2741
|
+
constructor(device) {
|
|
2742
|
+
this.device = device;
|
|
2743
|
+
}
|
|
2744
|
+
device;
|
|
2745
|
+
recordDraw() {
|
|
2746
|
+
this.device.totalDrawCount++;
|
|
2747
|
+
}
|
|
2748
|
+
recordDispatch() {
|
|
2749
|
+
this.device.totalDispatchCount++;
|
|
2750
|
+
}
|
|
2751
|
+
recordBindGroup() {
|
|
2752
|
+
this.device.totalBindGroupCount++;
|
|
2753
|
+
}
|
|
2754
|
+
recordPassName(name) {
|
|
2755
|
+
this.device.framePassNames.push(name);
|
|
2756
|
+
}
|
|
2757
|
+
};
|
|
2758
|
+
RhiNullCommandEncoder = class {
|
|
2759
|
+
bookkeeper;
|
|
2760
|
+
counter;
|
|
2761
|
+
constructor(bookkeeper, device) {
|
|
2762
|
+
this.bookkeeper = bookkeeper;
|
|
2763
|
+
this.counter = new DeviceCounter(device);
|
|
2764
|
+
}
|
|
2765
|
+
beginRenderPass(desc) {
|
|
2766
|
+
const label = readPassLabel(desc);
|
|
2767
|
+
return new RhiNullRenderPassEncoder(this.bookkeeper, this.counter, label);
|
|
2768
|
+
}
|
|
2769
|
+
beginComputePass(desc) {
|
|
2770
|
+
const label = readPassLabel(desc);
|
|
2771
|
+
return new RhiNullComputePassEncoder(this.bookkeeper, this.counter, label);
|
|
2772
|
+
}
|
|
2773
|
+
copyBufferToBuffer(_source, _sourceOffsetOrDestination, _destinationOrSize, _destinationOffset, _size) {
|
|
2774
|
+
}
|
|
2775
|
+
copyBufferToTexture(_source, _destination, _copySize) {
|
|
2776
|
+
}
|
|
2777
|
+
copyTextureToBuffer(_source, _destination, _copySize) {
|
|
2778
|
+
}
|
|
2779
|
+
copyTextureToTexture(_source, _destination, _copySize) {
|
|
2780
|
+
}
|
|
2781
|
+
clearBuffer(_buffer, _offset, _size) {
|
|
2782
|
+
}
|
|
2783
|
+
resolveQuerySet(_querySet, _firstQuery, _queryCount, _destination, _destinationOffset) {
|
|
2784
|
+
return ok(void 0);
|
|
2785
|
+
}
|
|
2786
|
+
writeTimestamp(_querySet, _queryIndex) {
|
|
2787
|
+
}
|
|
2788
|
+
pushDebugGroup(_groupLabel) {
|
|
2789
|
+
}
|
|
2790
|
+
popDebugGroup() {
|
|
2791
|
+
}
|
|
2792
|
+
insertDebugMarker(_markerLabel) {
|
|
2793
|
+
}
|
|
2794
|
+
finish() {
|
|
2795
|
+
return ok(this.bookkeeper.register("CommandBuffer"));
|
|
2796
|
+
}
|
|
2797
|
+
};
|
|
2798
|
+
BOOKKEEPING_KEY = /* @__PURE__ */ Symbol("forgeax-rhi-null-bookkeeping");
|
|
2799
|
+
Bookkeeper = class {
|
|
2800
|
+
deviceId;
|
|
2801
|
+
nextHandleId = 0;
|
|
2802
|
+
records = /* @__PURE__ */ new Map();
|
|
2803
|
+
constructor(deviceId) {
|
|
2804
|
+
this.deviceId = deviceId;
|
|
2805
|
+
}
|
|
2806
|
+
/**
|
|
2807
|
+
* Register a freshly-minted handle of the given kind, returning a plain
|
|
2808
|
+
* object that carries its ledger row. The caller casts the return value to
|
|
2809
|
+
* the concrete brand (`as unknown as Buffer` etc.).
|
|
2810
|
+
*/
|
|
2811
|
+
register(kind) {
|
|
2812
|
+
const id = this.nextHandleId++;
|
|
2813
|
+
const record = {
|
|
2814
|
+
id,
|
|
2815
|
+
kind,
|
|
2816
|
+
destroyed: false,
|
|
2817
|
+
sourceDeviceId: this.deviceId
|
|
2818
|
+
};
|
|
2819
|
+
this.records.set(id, record);
|
|
2820
|
+
return { [BOOKKEEPING_KEY]: record };
|
|
2821
|
+
}
|
|
2822
|
+
/**
|
|
2823
|
+
* Mark a handle destroyed. Fail-fasts on a second destroy
|
|
2824
|
+
* ('destroy-after-destroy') or on a handle issued by a different device
|
|
2825
|
+
* ('rhi-not-available'); otherwise flips the destroyed flag and returns ok.
|
|
2826
|
+
*/
|
|
2827
|
+
destroy(handle) {
|
|
2828
|
+
const ownership = this.validateOwnership(handle);
|
|
2829
|
+
if (!ownership.ok) return ownership;
|
|
2830
|
+
const destroyedCheck = isHandleDestroyed(handle);
|
|
2831
|
+
if (!destroyedCheck.ok) return destroyedCheck;
|
|
2832
|
+
ownership.value.destroyed = true;
|
|
2833
|
+
return ok(void 0);
|
|
2834
|
+
}
|
|
2835
|
+
/**
|
|
2836
|
+
* Report whether a handle has already been destroyed (true once destroy()
|
|
2837
|
+
* has flipped its flag). Used by command-stream methods that must not
|
|
2838
|
+
* consume a stale handle.
|
|
2839
|
+
*/
|
|
2840
|
+
isDestroyed(handle) {
|
|
2841
|
+
return readRecord(handle)?.destroyed === true;
|
|
2842
|
+
}
|
|
2843
|
+
/**
|
|
2844
|
+
* Validate that a handle was issued by THIS device. Returns the ledger row on
|
|
2845
|
+
* success so callers can mutate it (e.g. flip destroyed); returns
|
|
2846
|
+
* 'rhi-not-available' for a foreign handle (AC-09 — no silent pass).
|
|
2847
|
+
*/
|
|
2848
|
+
validateOwnership(handle) {
|
|
2849
|
+
const record = readRecord(handle);
|
|
2850
|
+
if (record === void 0 || record.sourceDeviceId !== this.deviceId) {
|
|
2851
|
+
return err(
|
|
2852
|
+
new RhiError({
|
|
2853
|
+
code: "rhi-not-available",
|
|
2854
|
+
expected: "handle was issued by this RhiNull device",
|
|
2855
|
+
hint: "do not pass a handle created on a different RhiNull device into this device; create resources on the device they are used with"
|
|
2856
|
+
})
|
|
2857
|
+
);
|
|
2858
|
+
}
|
|
2859
|
+
return ok(record);
|
|
2860
|
+
}
|
|
2861
|
+
/**
|
|
2862
|
+
* Return all ledger rows as a readonly array. M3 unit tests (w17) read this
|
|
2863
|
+
* to assert create/destroy pairing, BGL/PSO shape counts, and resource
|
|
2864
|
+
* lifecycle coverage (AC-05/06/07). Returns a snapshot of the current Map
|
|
2865
|
+
* so callers can iterate without a stale reference after further mutations.
|
|
2866
|
+
*/
|
|
2867
|
+
allRecords() {
|
|
2868
|
+
return [...this.records.values()];
|
|
2869
|
+
}
|
|
2870
|
+
/** Report the total number of records in the ledger. */
|
|
2871
|
+
recordCount() {
|
|
2872
|
+
return this.records.size;
|
|
2873
|
+
}
|
|
2874
|
+
};
|
|
2875
|
+
nextDeviceId = 0;
|
|
2876
|
+
RhiNullDevice = class {
|
|
2877
|
+
internalBookkeeper;
|
|
2878
|
+
nullQueue;
|
|
2879
|
+
encoderFactory;
|
|
2880
|
+
/** Per-frame total draw count across all pass encoders executed this frame
|
|
2881
|
+
* (aggregated by the command encoder on finish, then reset). M3 unit tests
|
|
2882
|
+
* (w17) read this to assert draw count >= 1 (AC-06). */
|
|
2883
|
+
totalDrawCount = 0;
|
|
2884
|
+
/** Per-frame total direct and indirect compute dispatch count. */
|
|
2885
|
+
totalDispatchCount = 0;
|
|
2886
|
+
/** Per-frame total bind group set count (AC-06 / AC-05 readback). */
|
|
2887
|
+
totalBindGroupCount = 0;
|
|
2888
|
+
/** Per-frame pass names executed this frame, in schedule order (AC-04). */
|
|
2889
|
+
framePassNames = [];
|
|
2890
|
+
/** The per-device handle ledger — exposed so M3 tests can assert create/destroy
|
|
2891
|
+
* pairing and BGL/PSO shape counts (AC-05/06/07). */
|
|
2892
|
+
get bookkeeper() {
|
|
2893
|
+
return this.internalBookkeeper;
|
|
2894
|
+
}
|
|
2895
|
+
constructor(queue, encoderFactory) {
|
|
2896
|
+
this.internalBookkeeper = new Bookkeeper(nextDeviceId++);
|
|
2897
|
+
this.nullQueue = queue;
|
|
2898
|
+
this.encoderFactory = encoderFactory;
|
|
2899
|
+
}
|
|
2900
|
+
get caps() {
|
|
2901
|
+
return {
|
|
2902
|
+
backendKind: "null",
|
|
2903
|
+
compute: true,
|
|
2904
|
+
timestampQuery: false,
|
|
2905
|
+
timestampPeriodNanoseconds: null,
|
|
2906
|
+
indirectDrawing: true,
|
|
2907
|
+
textureCompressionBc: false,
|
|
2908
|
+
textureCompressionEtc2: false,
|
|
2909
|
+
textureCompressionAstc: false,
|
|
2910
|
+
// 3 wgpu-native-only reserved flags stay false on non-native backends
|
|
2911
|
+
// (D-5); the headless backend is not a native runtime.
|
|
2912
|
+
multiDrawIndirect: false,
|
|
2913
|
+
pushConstants: false,
|
|
2914
|
+
textureBindingArray: false,
|
|
2915
|
+
samplerAliasing: true,
|
|
2916
|
+
firstInstanceIndirect: true,
|
|
2917
|
+
storageBuffer: true,
|
|
2918
|
+
storageTexture: true,
|
|
2919
|
+
rgba16floatRenderable: true,
|
|
2920
|
+
rg11b10ufloatRenderable: true,
|
|
2921
|
+
float32Filterable: true,
|
|
2922
|
+
maxColorAttachments: 8
|
|
2923
|
+
};
|
|
2924
|
+
}
|
|
2925
|
+
get features() {
|
|
2926
|
+
return EMPTY_FEATURES;
|
|
2927
|
+
}
|
|
2928
|
+
get limits() {
|
|
2929
|
+
return EMPTY_LIMITS;
|
|
2930
|
+
}
|
|
2931
|
+
get queue() {
|
|
2932
|
+
return this.nullQueue;
|
|
2933
|
+
}
|
|
2934
|
+
// forgeax-async-whitelist: dom-native — spec `GPUDevice.lost` Promise
|
|
2935
|
+
// passthrough. The headless backend never loses a device (no GPU), so the
|
|
2936
|
+
// Promise stays unsettled for the lifetime of the device, mirroring a live
|
|
2937
|
+
// device that never transitions to the lost state.
|
|
2938
|
+
get lost() {
|
|
2939
|
+
return NEVER;
|
|
2940
|
+
}
|
|
2941
|
+
createBuffer(_desc) {
|
|
2942
|
+
return ok(this.internalBookkeeper.register("Buffer"));
|
|
2943
|
+
}
|
|
2944
|
+
createTexture(_desc) {
|
|
2945
|
+
return ok(this.internalBookkeeper.register("Texture"));
|
|
2946
|
+
}
|
|
2947
|
+
destroyBuffer(buf) {
|
|
2948
|
+
return this.internalBookkeeper.destroy(buf);
|
|
2949
|
+
}
|
|
2950
|
+
destroyQuerySet(querySet) {
|
|
2951
|
+
return this.internalBookkeeper.destroy(querySet);
|
|
2952
|
+
}
|
|
2953
|
+
destroyTexture(tex) {
|
|
2954
|
+
return this.internalBookkeeper.destroy(tex);
|
|
2955
|
+
}
|
|
2956
|
+
createTextureView(_texture, _desc) {
|
|
2957
|
+
return ok(this.internalBookkeeper.register("TextureView"));
|
|
2958
|
+
}
|
|
2959
|
+
createSampler(_desc) {
|
|
2960
|
+
return ok(this.internalBookkeeper.register("Sampler"));
|
|
2961
|
+
}
|
|
2962
|
+
createBindGroupLayout(_desc) {
|
|
2963
|
+
return ok(this.internalBookkeeper.register("BindGroupLayout"));
|
|
2964
|
+
}
|
|
2965
|
+
createBindGroup(_desc) {
|
|
2966
|
+
return ok(this.internalBookkeeper.register("BindGroup"));
|
|
2967
|
+
}
|
|
2968
|
+
createPipelineLayout(_desc) {
|
|
2969
|
+
return ok(this.internalBookkeeper.register("PipelineLayout"));
|
|
2970
|
+
}
|
|
2971
|
+
createRenderPipeline(_desc) {
|
|
2972
|
+
return ok(this.makePipeline("RenderPipeline"));
|
|
2973
|
+
}
|
|
2974
|
+
createComputePipeline(_desc) {
|
|
2975
|
+
return ok(this.makePipeline("ComputePipeline"));
|
|
2976
|
+
}
|
|
2977
|
+
createQuerySet(desc) {
|
|
2978
|
+
if (desc.type === "timestamp") {
|
|
2979
|
+
return err(
|
|
2980
|
+
new RhiError({
|
|
2981
|
+
code: "feature-not-enabled",
|
|
2982
|
+
expected: "caps.timestampQuery === true (timestamp-query feature)",
|
|
2983
|
+
hint: "RhiNull is structural-only and cannot produce GPU timestamp ticks"
|
|
2984
|
+
})
|
|
2985
|
+
);
|
|
2986
|
+
}
|
|
2987
|
+
return ok(this.internalBookkeeper.register("QuerySet"));
|
|
2988
|
+
}
|
|
2989
|
+
createCommandEncoder(_desc) {
|
|
2990
|
+
return ok(this.encoderFactory(this.internalBookkeeper, this));
|
|
2991
|
+
}
|
|
2992
|
+
/**
|
|
2993
|
+
* Mint a pipeline handle whose object also carries the no-op
|
|
2994
|
+
* `getBindGroupLayout(index)` ops method (D-2). The prod auto-layout path
|
|
2995
|
+
* (debug-draw.ts) and the existing mock unit tests both call
|
|
2996
|
+
* `pipeline.getBindGroupLayout(n)`; returning a legal BindGroupLayout brand
|
|
2997
|
+
* (recorded in the ledger) keeps those consumers from crashing on a missing
|
|
2998
|
+
* method.
|
|
2999
|
+
*/
|
|
3000
|
+
makePipeline(kind) {
|
|
3001
|
+
const handle = this.internalBookkeeper.register(kind);
|
|
3002
|
+
const getBindGroupLayout = (_index) => this.internalBookkeeper.register("BindGroupLayout");
|
|
3003
|
+
return Object.assign(handle, { getBindGroupLayout });
|
|
3004
|
+
}
|
|
3005
|
+
};
|
|
3006
|
+
EMPTY_FEATURES = /* @__PURE__ */ new Set();
|
|
3007
|
+
EMPTY_LIMITS = {};
|
|
3008
|
+
NEVER = new Promise(() => {
|
|
3009
|
+
});
|
|
3010
|
+
RhiNullQueue = class {
|
|
3011
|
+
writeBuffer(_buffer, _bufferOffset, _data, _dataOffset, _size) {
|
|
3012
|
+
return ok(void 0);
|
|
3013
|
+
}
|
|
3014
|
+
writeTexture(_destination, _data, _dataLayout, _size) {
|
|
3015
|
+
return ok(void 0);
|
|
3016
|
+
}
|
|
3017
|
+
copyExternalImageToTexture(_source, _destination, _copySize) {
|
|
3018
|
+
return ok(void 0);
|
|
3019
|
+
}
|
|
3020
|
+
submit(_commandBuffers) {
|
|
3021
|
+
return ok(void 0);
|
|
3022
|
+
}
|
|
3023
|
+
// forgeax-async-whitelist: dom-native — spec `GPUQueue.onSubmittedWorkDone`
|
|
3024
|
+
// never rejects. The headless backend has no pending GPU work, so it resolves
|
|
3025
|
+
// immediately (AC-12: read-back idioms must not hang).
|
|
3026
|
+
onSubmittedWorkDone() {
|
|
3027
|
+
return Promise.resolve(void 0);
|
|
3028
|
+
}
|
|
3029
|
+
};
|
|
3030
|
+
RhiNullAdapter = class {
|
|
3031
|
+
features = /* @__PURE__ */ new Set();
|
|
3032
|
+
limits = {};
|
|
3033
|
+
// forgeax-async-whitelist is not needed: this returns Promise<Result<...>>
|
|
3034
|
+
// per the spec contract; never rejects.
|
|
3035
|
+
requestDevice(_opts) {
|
|
3036
|
+
const device = new RhiNullDevice(
|
|
3037
|
+
new RhiNullQueue(),
|
|
3038
|
+
(bookkeeper, dev) => new RhiNullCommandEncoder(bookkeeper, dev)
|
|
3039
|
+
);
|
|
3040
|
+
return Promise.resolve(ok(device));
|
|
3041
|
+
}
|
|
3042
|
+
};
|
|
3043
|
+
RhiNullCanvasContext = class {
|
|
3044
|
+
configure(_desc) {
|
|
3045
|
+
return ok(void 0);
|
|
3046
|
+
}
|
|
3047
|
+
unconfigure() {
|
|
3048
|
+
}
|
|
3049
|
+
getConfiguration() {
|
|
3050
|
+
return void 0;
|
|
3051
|
+
}
|
|
3052
|
+
getCurrentTexture() {
|
|
3053
|
+
return ok({});
|
|
3054
|
+
}
|
|
3055
|
+
};
|
|
3056
|
+
rhi = {
|
|
3057
|
+
requestAdapter,
|
|
3058
|
+
acquireCanvasContext,
|
|
3059
|
+
createShaderModule
|
|
3060
|
+
};
|
|
3061
|
+
}
|
|
3062
|
+
});
|
|
3063
|
+
function createCliRhiDebugOperationContext() {
|
|
3064
|
+
return {
|
|
3065
|
+
captureFrame: async () => err(
|
|
3066
|
+
createRhiDebugError("capture-unavailable", {
|
|
3067
|
+
stage: "capture",
|
|
3068
|
+
cause: "the standalone CLI has no live App capture provider"
|
|
3069
|
+
})
|
|
3070
|
+
),
|
|
3071
|
+
async readArtifact(artifact) {
|
|
3072
|
+
if (artifact.path === void 0) {
|
|
3073
|
+
return {
|
|
3074
|
+
ok: false,
|
|
3075
|
+
error: {
|
|
3076
|
+
code: "artifact-path-missing",
|
|
3077
|
+
expected: "ArtifactRef.path to identify a readable .rhitape file",
|
|
3078
|
+
hint: "Pass the path returned by the capture host together with its digest.",
|
|
3079
|
+
detail: { digest: artifact.digest }
|
|
3080
|
+
}
|
|
3081
|
+
};
|
|
3082
|
+
}
|
|
3083
|
+
try {
|
|
3084
|
+
return { ok: true, value: new Uint8Array(await readFile(artifact.path)) };
|
|
3085
|
+
} catch (cause) {
|
|
3086
|
+
return {
|
|
3087
|
+
ok: false,
|
|
3088
|
+
error: {
|
|
3089
|
+
code: "artifact-read-failed",
|
|
3090
|
+
expected: "the ArtifactRef path to be readable",
|
|
3091
|
+
hint: "Check the tape path and recapture if the artifact was removed.",
|
|
3092
|
+
detail: {
|
|
3093
|
+
path: artifact.path,
|
|
3094
|
+
reason: cause instanceof Error ? cause.message : String(cause)
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
};
|
|
3098
|
+
}
|
|
3099
|
+
},
|
|
3100
|
+
async createReplayBackend() {
|
|
3101
|
+
const adapter = await rhi.requestAdapter();
|
|
3102
|
+
if (!adapter.ok) {
|
|
3103
|
+
return {
|
|
3104
|
+
ok: false,
|
|
3105
|
+
error: {
|
|
3106
|
+
code: "replay-backend-unavailable",
|
|
3107
|
+
expected: "a fresh rhi-null adapter",
|
|
3108
|
+
hint: adapter.error.hint,
|
|
3109
|
+
detail: { stage: "adapter", cause: adapter.error.hint }
|
|
3110
|
+
}
|
|
3111
|
+
};
|
|
3112
|
+
}
|
|
3113
|
+
const device = await adapter.value.requestDevice();
|
|
3114
|
+
if (!device.ok) {
|
|
3115
|
+
return {
|
|
3116
|
+
ok: false,
|
|
3117
|
+
error: {
|
|
3118
|
+
code: "replay-backend-unavailable",
|
|
3119
|
+
expected: "a fresh rhi-null device",
|
|
3120
|
+
hint: device.error.hint,
|
|
3121
|
+
detail: { stage: "device", cause: device.error.hint }
|
|
3122
|
+
}
|
|
3123
|
+
};
|
|
3124
|
+
}
|
|
3125
|
+
return { ok: true, value: { device: device.value, createShaderModule } };
|
|
3126
|
+
}
|
|
3127
|
+
};
|
|
3128
|
+
}
|
|
3129
|
+
var init_cli_context = __esm({
|
|
3130
|
+
"src/rhi-debug/cli-context.ts"() {
|
|
3131
|
+
init_dist3();
|
|
3132
|
+
}
|
|
3133
|
+
});
|
|
3134
|
+
function createRhiDebugOperationContext(host) {
|
|
3135
|
+
return {
|
|
3136
|
+
async captureFrame(input) {
|
|
3137
|
+
const captured = await host.captureFrame(input);
|
|
3138
|
+
if (!captured.ok) return captured;
|
|
3139
|
+
return ok({
|
|
3140
|
+
kind: RHI_TAPE_ARTIFACT_KIND,
|
|
3141
|
+
digest: captured.value.digest,
|
|
3142
|
+
source: captured.value.source ?? "rhi.capture",
|
|
3143
|
+
...captured.value.path === void 0 ? {} : { path: captured.value.path },
|
|
3144
|
+
bytes: captured.value.bytes
|
|
3145
|
+
});
|
|
3146
|
+
},
|
|
3147
|
+
readArtifact: host.readArtifact,
|
|
3148
|
+
...host.createReplayBackend === void 0 ? {} : { createReplayBackend: host.createReplayBackend }
|
|
3149
|
+
};
|
|
3150
|
+
}
|
|
3151
|
+
function discoverRhiDebugOperations() {
|
|
3152
|
+
return RHI_DEBUG_OPERATION_MANIFEST.operations;
|
|
3153
|
+
}
|
|
3154
|
+
function renderRhiDebugHelp() {
|
|
3155
|
+
return [
|
|
3156
|
+
"forgeax run <operation>",
|
|
3157
|
+
...discoverRhiDebugOperations().map((operation) => ` ${operation.name}: ${operation.summary}`),
|
|
3158
|
+
"ArtifactRef schema:",
|
|
3159
|
+
JSON.stringify(RHI_DEBUG_OPERATION_MANIFEST.artifactRefSchema)
|
|
3160
|
+
].join("\n");
|
|
3161
|
+
}
|
|
3162
|
+
function recoverRhiDebugError(error) {
|
|
3163
|
+
switch (error.code) {
|
|
3164
|
+
case "capture-unavailable":
|
|
3165
|
+
case "capture-busy":
|
|
3166
|
+
case "capture-snapshot-failed":
|
|
3167
|
+
case "capture-timeout":
|
|
3168
|
+
return `${error.hint}: ${error.detail?.cause ?? "capture failed"}`;
|
|
3169
|
+
case "tape-invalid":
|
|
3170
|
+
return `${error.hint}: ${error.detail?.cause ?? "tape validation failed"}`;
|
|
3171
|
+
case "tape-version-unsupported":
|
|
3172
|
+
return `${error.hint}: found ${error.detail?.foundVersion}, expected ${error.detail?.expectedVersion}`;
|
|
3173
|
+
case "replay-capability-mismatch":
|
|
3174
|
+
return `${error.hint}: ${error.detail?.cause ?? "capability mismatch"}`;
|
|
3175
|
+
case "replay-event-failed":
|
|
3176
|
+
return `${error.hint}: event ${error.detail?.eventIndex} ${error.detail?.kind} at ${error.detail?.stage}`;
|
|
3177
|
+
case "replay-position-invalid":
|
|
3178
|
+
return `${error.hint}: requested ${error.detail?.requested}, available ${error.detail?.available}`;
|
|
3179
|
+
case "readback-failed":
|
|
3180
|
+
return `${error.hint}: ${error.detail?.cause ?? "readback failed"}`;
|
|
3181
|
+
case "readback-unsupported":
|
|
3182
|
+
return `${error.hint}: ${error.detail?.reason ?? "readback is unsupported"}`;
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
function coreError(error) {
|
|
3186
|
+
return {
|
|
3187
|
+
code: error.code,
|
|
3188
|
+
expected: error.expected,
|
|
3189
|
+
hint: recoverRhiDebugError(error),
|
|
3190
|
+
detail: error.detail === void 0 ? {} : Object.fromEntries(Object.entries(error.detail))
|
|
3191
|
+
};
|
|
3192
|
+
}
|
|
3193
|
+
function operationError(code, expected, hint, detail = {}) {
|
|
3194
|
+
return { ok: false, error: { code, expected, hint, detail } };
|
|
3195
|
+
}
|
|
3196
|
+
function validateArtifactRef(value) {
|
|
3197
|
+
if (value === null || typeof value !== "object") {
|
|
3198
|
+
return operationError(
|
|
3199
|
+
"artifact-ref-invalid",
|
|
3200
|
+
"an ArtifactRef object",
|
|
3201
|
+
"Pass the ArtifactRef returned by rhi.capture unchanged."
|
|
3202
|
+
);
|
|
3203
|
+
}
|
|
3204
|
+
const candidate = value;
|
|
3205
|
+
if (candidate.kind !== RHI_TAPE_ARTIFACT_KIND) {
|
|
3206
|
+
return operationError(
|
|
3207
|
+
"artifact-kind-invalid",
|
|
3208
|
+
`ArtifactRef.kind to equal ${RHI_TAPE_ARTIFACT_KIND}`,
|
|
3209
|
+
"Capture a new RHI tape and pass its ArtifactRef unchanged.",
|
|
3210
|
+
{ kind: candidate.kind ?? null }
|
|
3211
|
+
);
|
|
3212
|
+
}
|
|
3213
|
+
if (typeof candidate.digest !== "string" || candidate.digest.length === 0) {
|
|
3214
|
+
return operationError(
|
|
3215
|
+
"artifact-digest-invalid",
|
|
3216
|
+
"ArtifactRef.digest to be a non-empty digest",
|
|
3217
|
+
"Use the digest returned with the captured tape."
|
|
3218
|
+
);
|
|
3219
|
+
}
|
|
3220
|
+
if (typeof candidate.source !== "string" || candidate.source.length === 0) {
|
|
3221
|
+
return operationError(
|
|
3222
|
+
"artifact-source-invalid",
|
|
3223
|
+
"ArtifactRef.source to identify the producing operation",
|
|
3224
|
+
"Use the complete ArtifactRef returned by rhi.capture."
|
|
3225
|
+
);
|
|
3226
|
+
}
|
|
3227
|
+
return {
|
|
3228
|
+
ok: true,
|
|
3229
|
+
value: {
|
|
3230
|
+
kind: RHI_TAPE_ARTIFACT_KIND,
|
|
3231
|
+
digest: candidate.digest,
|
|
3232
|
+
source: candidate.source,
|
|
3233
|
+
...typeof candidate.path === "string" ? { path: candidate.path } : {}
|
|
3234
|
+
}
|
|
3235
|
+
};
|
|
3236
|
+
}
|
|
3237
|
+
async function readTape(artifact, context) {
|
|
3238
|
+
const validated = validateArtifactRef(artifact);
|
|
3239
|
+
if (!validated.ok) return validated;
|
|
3240
|
+
return context.readArtifact(validated.value);
|
|
3241
|
+
}
|
|
3242
|
+
async function runRhiDebugOperation(name, input, context) {
|
|
3243
|
+
switch (name) {
|
|
3244
|
+
case "rhi.capture": {
|
|
3245
|
+
const capture = await context.captureFrame(input);
|
|
3246
|
+
if (!capture.ok) return { ok: false, error: coreError(capture.error) };
|
|
3247
|
+
const artifact = validateArtifactRef(capture.value);
|
|
3248
|
+
if (!artifact.ok) return artifact;
|
|
3249
|
+
return { ok: true, value: artifact.value };
|
|
3250
|
+
}
|
|
3251
|
+
case "rhi.summary": {
|
|
3252
|
+
const summaryInput = input;
|
|
3253
|
+
const bytes = await readTape(summaryInput.artifact, context);
|
|
3254
|
+
if (!bytes.ok) return bytes;
|
|
3255
|
+
const decoded = decodeTape(bytes.value);
|
|
3256
|
+
if (!decoded.ok) return { ok: false, error: coreError(decoded.error) };
|
|
3257
|
+
const artifact = validateArtifactRef(summaryInput.artifact);
|
|
3258
|
+
if (!artifact.ok) return artifact;
|
|
3259
|
+
return {
|
|
3260
|
+
ok: true,
|
|
3261
|
+
value: { artifact: artifact.value, model: buildFrameModel(decoded.value) }
|
|
3262
|
+
};
|
|
3263
|
+
}
|
|
3264
|
+
case "rhi.inspect": {
|
|
3265
|
+
const inspectInput = input;
|
|
3266
|
+
if (!Number.isInteger(inspectInput.workIndex) || inspectInput.workIndex < 0) {
|
|
3267
|
+
return operationError(
|
|
3268
|
+
"work-index-invalid",
|
|
3269
|
+
"workIndex to be a non-negative integer",
|
|
3270
|
+
"Choose workIndex from the FrameModel returned by rhi.summary.",
|
|
3271
|
+
{ workIndex: inspectInput.workIndex }
|
|
3272
|
+
);
|
|
3273
|
+
}
|
|
3274
|
+
const bytes = await readTape(inspectInput.artifact, context);
|
|
3275
|
+
if (!bytes.ok) return bytes;
|
|
3276
|
+
const decoded = decodeTape(bytes.value);
|
|
3277
|
+
if (!decoded.ok) return { ok: false, error: coreError(decoded.error) };
|
|
3278
|
+
if (context.createReplayBackend === void 0) {
|
|
3279
|
+
return operationError(
|
|
3280
|
+
"replay-backend-unavailable",
|
|
3281
|
+
"a fresh ReplayBackend factory",
|
|
3282
|
+
"Provide a fresh device and shader factory before running rhi.inspect."
|
|
3283
|
+
);
|
|
3284
|
+
}
|
|
3285
|
+
const backend = await context.createReplayBackend();
|
|
3286
|
+
if (!backend.ok) return backend;
|
|
3287
|
+
const opened = await openReplay(decoded.value, backend.value);
|
|
3288
|
+
if (!opened.ok) return { ok: false, error: coreError(opened.error) };
|
|
3289
|
+
try {
|
|
3290
|
+
const inspection = await opened.value.inspectWork(
|
|
3291
|
+
inspectInput.workIndex,
|
|
3292
|
+
inspectInput.fields
|
|
3293
|
+
);
|
|
3294
|
+
if (!inspection.ok) return { ok: false, error: coreError(inspection.error) };
|
|
3295
|
+
const artifact = validateArtifactRef(inspectInput.artifact);
|
|
3296
|
+
if (!artifact.ok) return artifact;
|
|
3297
|
+
return { ok: true, value: { artifact: artifact.value, inspection: inspection.value } };
|
|
3298
|
+
} finally {
|
|
3299
|
+
await opened.value.dispose();
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
3304
|
+
var RHI_TAPE_ARTIFACT_KIND, artifactRefSchema, summaryOutputSchema, inspectOutputSchema, RHI_DEBUG_OPERATION_MANIFEST;
|
|
3305
|
+
var init_operations = __esm({
|
|
3306
|
+
"src/rhi-debug/operations.ts"() {
|
|
3307
|
+
RHI_TAPE_ARTIFACT_KIND = "rhi-tape";
|
|
3308
|
+
artifactRefSchema = {
|
|
3309
|
+
type: "object",
|
|
3310
|
+
properties: {
|
|
3311
|
+
kind: { type: "string", enum: [RHI_TAPE_ARTIFACT_KIND] },
|
|
3312
|
+
digest: { type: "string" },
|
|
3313
|
+
source: { type: "string" },
|
|
3314
|
+
path: { type: "string" }
|
|
3315
|
+
},
|
|
3316
|
+
required: ["kind", "digest", "source"],
|
|
3317
|
+
additionalProperties: false
|
|
3318
|
+
};
|
|
3319
|
+
summaryOutputSchema = {
|
|
3320
|
+
type: "object",
|
|
3321
|
+
properties: {
|
|
3322
|
+
artifact: artifactRefSchema,
|
|
3323
|
+
model: { type: "object", additionalProperties: true }
|
|
3324
|
+
},
|
|
3325
|
+
required: ["artifact", "model"],
|
|
3326
|
+
additionalProperties: false
|
|
3327
|
+
};
|
|
3328
|
+
inspectOutputSchema = {
|
|
3329
|
+
type: "object",
|
|
3330
|
+
properties: {
|
|
3331
|
+
artifact: artifactRefSchema,
|
|
3332
|
+
inspection: { type: "object", additionalProperties: true }
|
|
3333
|
+
},
|
|
3334
|
+
required: ["artifact", "inspection"],
|
|
3335
|
+
additionalProperties: false
|
|
3336
|
+
};
|
|
3337
|
+
RHI_DEBUG_OPERATION_MANIFEST = {
|
|
3338
|
+
schemaVersion: "1.0.0",
|
|
3339
|
+
artifactRefSchema,
|
|
3340
|
+
operations: [
|
|
3341
|
+
{
|
|
3342
|
+
name: "rhi.capture",
|
|
3343
|
+
summary: "Capture the next frame and return one rhi-tape ArtifactRef.",
|
|
3344
|
+
inputSchema: {
|
|
3345
|
+
type: "object",
|
|
3346
|
+
properties: {},
|
|
3347
|
+
additionalProperties: false
|
|
3348
|
+
},
|
|
3349
|
+
outputSchema: artifactRefSchema,
|
|
3350
|
+
recoveryCodes: [
|
|
3351
|
+
"capture-unavailable",
|
|
3352
|
+
"capture-busy",
|
|
3353
|
+
"capture-snapshot-failed",
|
|
3354
|
+
"capture-timeout"
|
|
3355
|
+
]
|
|
3356
|
+
},
|
|
3357
|
+
{
|
|
3358
|
+
name: "rhi.summary",
|
|
3359
|
+
summary: "Strictly decode one ArtifactRef and return its CPU FrameModel.",
|
|
3360
|
+
inputSchema: {
|
|
3361
|
+
type: "object",
|
|
3362
|
+
properties: { artifact: artifactRefSchema },
|
|
3363
|
+
required: ["artifact"],
|
|
3364
|
+
additionalProperties: false
|
|
3365
|
+
},
|
|
3366
|
+
outputSchema: summaryOutputSchema,
|
|
3367
|
+
recoveryCodes: ["tape-invalid", "tape-version-unsupported"]
|
|
3368
|
+
},
|
|
3369
|
+
{
|
|
3370
|
+
name: "rhi.inspect",
|
|
3371
|
+
summary: "Replay one ArtifactRef on a fresh backend and inspect a workIndex.",
|
|
3372
|
+
inputSchema: {
|
|
3373
|
+
type: "object",
|
|
3374
|
+
properties: {
|
|
3375
|
+
artifact: artifactRefSchema,
|
|
3376
|
+
workIndex: { type: "integer", minimum: 0 },
|
|
3377
|
+
fields: { type: "array", items: { type: "string" } }
|
|
3378
|
+
},
|
|
3379
|
+
required: ["artifact", "workIndex"],
|
|
3380
|
+
additionalProperties: false
|
|
3381
|
+
},
|
|
3382
|
+
outputSchema: inspectOutputSchema,
|
|
3383
|
+
recoveryCodes: [
|
|
3384
|
+
"tape-invalid",
|
|
3385
|
+
"tape-version-unsupported",
|
|
3386
|
+
"replay-capability-mismatch",
|
|
3387
|
+
"replay-event-failed",
|
|
3388
|
+
"replay-position-invalid",
|
|
3389
|
+
"readback-failed",
|
|
3390
|
+
"readback-unsupported"
|
|
3391
|
+
]
|
|
3392
|
+
}
|
|
3393
|
+
]
|
|
3394
|
+
};
|
|
3395
|
+
}
|
|
3396
|
+
});
|
|
3397
|
+
async function packageVersion() {
|
|
3398
|
+
const manifest = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
|
|
3399
|
+
if (typeof manifest.version !== "string" || manifest.version.length === 0) {
|
|
3400
|
+
throw new Error("sdk-installer-version-missing");
|
|
3401
|
+
}
|
|
3402
|
+
return manifest.version;
|
|
3403
|
+
}
|
|
3404
|
+
async function sdkInstallCommand(options) {
|
|
3405
|
+
const root = resolve(options.root);
|
|
3406
|
+
let targetExists = true;
|
|
3407
|
+
let entries2;
|
|
3408
|
+
try {
|
|
3409
|
+
entries2 = await readdir(root);
|
|
3410
|
+
} catch (cause) {
|
|
3411
|
+
if (cause === null || typeof cause !== "object" || !("code" in cause) || cause.code !== "ENOENT") {
|
|
3412
|
+
return { ok: false, error: commandError(cause, "sdk-install-failed") };
|
|
3413
|
+
}
|
|
3414
|
+
targetExists = false;
|
|
3415
|
+
entries2 = [];
|
|
3416
|
+
}
|
|
3417
|
+
if (entries2.length > 0) {
|
|
3418
|
+
return {
|
|
3419
|
+
ok: false,
|
|
3420
|
+
error: {
|
|
3421
|
+
code: "sdk-target-not-empty",
|
|
3422
|
+
expected: "the SDK install target to be absent or empty",
|
|
3423
|
+
hint: "Choose an empty versioned SDK directory so existing files cannot be overwritten.",
|
|
3424
|
+
detail: { root }
|
|
3425
|
+
}
|
|
3426
|
+
};
|
|
3427
|
+
}
|
|
3428
|
+
const version = options.version ?? await packageVersion();
|
|
3429
|
+
const parent = dirname(root);
|
|
3430
|
+
await mkdir(parent, { recursive: true });
|
|
3431
|
+
let staging;
|
|
3432
|
+
let download;
|
|
3433
|
+
try {
|
|
3434
|
+
staging = await mkdtemp(resolve(parent, `.${basename(root)}.forgeax-sdk-staging-`));
|
|
3435
|
+
download = await mkdtemp(resolve(parent, `.${basename(root)}.forgeax-sdk-download-`));
|
|
3436
|
+
await execFileAsync3(
|
|
3437
|
+
process.env.FORGEAX_NPM_CLIENT ?? "npm",
|
|
3438
|
+
[
|
|
3439
|
+
"install",
|
|
3440
|
+
"--prefix",
|
|
3441
|
+
download,
|
|
3442
|
+
"--ignore-scripts",
|
|
3443
|
+
"--no-package-lock",
|
|
3444
|
+
"--no-save",
|
|
3445
|
+
"--fund=false",
|
|
3446
|
+
"--audit=false",
|
|
3447
|
+
`@forgeax/engine-sdk@${version}`
|
|
3448
|
+
],
|
|
3449
|
+
{ env: { ...process.env, CI: "true" }, maxBuffer: 16 * 1024 * 1024 }
|
|
3450
|
+
);
|
|
3451
|
+
const carrier = resolve(download, "node_modules", "@forgeax", "engine-sdk", "sdk");
|
|
3452
|
+
const manifest = JSON.parse(await readFile(resolve(carrier, "sdk-manifest.json"), "utf8"));
|
|
3453
|
+
if (manifest.sdkVersion !== version) throw new Error("sdk-carrier-version-mismatch");
|
|
3454
|
+
for (const name of await readdir(carrier)) {
|
|
3455
|
+
await cp(resolve(carrier, name), resolve(staging, name), {
|
|
3456
|
+
recursive: true,
|
|
3457
|
+
errorOnExist: true,
|
|
3458
|
+
force: false
|
|
3459
|
+
});
|
|
3460
|
+
}
|
|
3461
|
+
if (!targetExists) {
|
|
3462
|
+
await rename(staging, root);
|
|
3463
|
+
staging = void 0;
|
|
3464
|
+
} else {
|
|
3465
|
+
for (const name of await readdir(staging)) {
|
|
3466
|
+
await rename(resolve(staging, name), resolve(root, name));
|
|
3467
|
+
}
|
|
3468
|
+
}
|
|
3469
|
+
return { ok: true, value: { root, sdkVersion: version, source: "@forgeax/engine-sdk" } };
|
|
3470
|
+
} catch (cause) {
|
|
3471
|
+
return { ok: false, error: commandError(cause, "sdk-install-failed") };
|
|
3472
|
+
} finally {
|
|
3473
|
+
if (staging !== void 0) await rm(staging, { recursive: true, force: true });
|
|
3474
|
+
if (download !== void 0) await rm(download, { recursive: true, force: true });
|
|
3475
|
+
}
|
|
3476
|
+
}
|
|
3477
|
+
var execFileAsync3;
|
|
3478
|
+
var init_sdk_install = __esm({
|
|
3479
|
+
"src/sdk-install.ts"() {
|
|
3480
|
+
init_project();
|
|
3481
|
+
execFileAsync3 = promisify(execFile);
|
|
3482
|
+
}
|
|
3483
|
+
});
|
|
3484
|
+
async function shaderCheckCommand(options = {}) {
|
|
3485
|
+
const facts = await readProjectFacts(options.root);
|
|
3486
|
+
if (!facts.ok) return facts;
|
|
3487
|
+
const target = options.path === void 0 ? void 0 : resolve(facts.value.root, options.path);
|
|
3488
|
+
if (target !== void 0) {
|
|
3489
|
+
try {
|
|
3490
|
+
await readFile(target);
|
|
3491
|
+
} catch (cause) {
|
|
3492
|
+
return { ok: false, error: commandError(cause, "shader-source-unreadable") };
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3495
|
+
const temporary = await mkdtemp(resolve(tmpdir(), "forgeax-shader-check-"));
|
|
3496
|
+
const previous = process.cwd();
|
|
3497
|
+
try {
|
|
3498
|
+
process.chdir(facts.value.root);
|
|
3499
|
+
const config = await createViteConfig(facts.value, "build");
|
|
3500
|
+
await build({
|
|
3501
|
+
...config,
|
|
3502
|
+
build: { ...config.build, outDir: temporary, emptyOutDir: true, write: true }
|
|
3503
|
+
});
|
|
3504
|
+
return { ok: true, value: { root: facts.value.root, path: target ?? null } };
|
|
3505
|
+
} catch (cause) {
|
|
3506
|
+
return { ok: false, error: commandError(cause, "shader-check-failed") };
|
|
3507
|
+
} finally {
|
|
3508
|
+
process.chdir(previous);
|
|
3509
|
+
await rm(temporary, { recursive: true, force: true });
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3512
|
+
var init_shader_check = __esm({
|
|
3513
|
+
"src/shader-check.ts"() {
|
|
3514
|
+
init_host();
|
|
3515
|
+
init_project();
|
|
3516
|
+
}
|
|
3517
|
+
});
|
|
3518
|
+
|
|
3519
|
+
// src/commands.ts
|
|
3520
|
+
var commands_exports = {};
|
|
3521
|
+
__export(commands_exports, {
|
|
3522
|
+
RHI_DEBUG_OPERATION_MANIFEST: () => RHI_DEBUG_OPERATION_MANIFEST,
|
|
3523
|
+
assetAddCommand: () => assetAddCommand,
|
|
3524
|
+
assetInspectCommand: () => assetInspectCommand,
|
|
3525
|
+
assetListCommand: () => assetListCommand,
|
|
3526
|
+
assetVerifyCommand: () => assetVerifyCommand,
|
|
3527
|
+
buildCommand: () => buildCommand,
|
|
3528
|
+
createCliRhiDebugOperationContext: () => createCliRhiDebugOperationContext,
|
|
3529
|
+
createRhiDebugOperationContext: () => createRhiDebugOperationContext,
|
|
3530
|
+
devCommand: () => devCommand,
|
|
3531
|
+
discoverRhiDebugOperations: () => discoverRhiDebugOperations,
|
|
3532
|
+
doctorCommand: () => doctorCommand,
|
|
3533
|
+
initCommand: () => initCommand,
|
|
3534
|
+
newCommand: () => newCommand,
|
|
3535
|
+
packageCommand: () => packageCommand,
|
|
3536
|
+
pluginInstallCommand: () => pluginInstallCommand,
|
|
3537
|
+
pluginUninstallCommand: () => pluginUninstallCommand,
|
|
3538
|
+
previewCommand: () => previewCommand,
|
|
3539
|
+
recoverRhiDebugError: () => recoverRhiDebugError,
|
|
3540
|
+
renderRhiDebugHelp: () => renderRhiDebugHelp,
|
|
3541
|
+
runRhiDebugCommand: () => runRhiDebugCommand,
|
|
3542
|
+
runRhiDebugOperation: () => runRhiDebugOperation,
|
|
3543
|
+
sdkInstallCommand: () => sdkInstallCommand,
|
|
3544
|
+
shaderCheckCommand: () => shaderCheckCommand,
|
|
3545
|
+
skillInstallCommand: () => skillInstallCommand,
|
|
3546
|
+
skillVerifyCommand: () => skillVerifyCommand,
|
|
3547
|
+
testCommand: () => testCommand
|
|
3548
|
+
});
|
|
3549
|
+
function runRhiDebugCommand(name, input, context) {
|
|
3550
|
+
return runRhiDebugOperation(name, input, context);
|
|
3551
|
+
}
|
|
3552
|
+
async function buildCommand(options = {}) {
|
|
3553
|
+
const facts = await readProjectFacts(options.root);
|
|
3554
|
+
if (!facts.ok) return facts;
|
|
3555
|
+
const previous = process.cwd();
|
|
3556
|
+
const base = options.base ?? "/";
|
|
3557
|
+
const outDir = resolve(facts.value.root, options.outDir ?? "dist");
|
|
3558
|
+
try {
|
|
3559
|
+
process.chdir(facts.value.root);
|
|
3560
|
+
await build(await createViteConfig(facts.value, "build", base, { outDir }));
|
|
3561
|
+
return { ok: true, value: await writeDistManifest(facts.value, base, outDir) };
|
|
3562
|
+
} catch (cause) {
|
|
3563
|
+
return { ok: false, error: commandError(cause, "game-build-failed") };
|
|
3564
|
+
} finally {
|
|
3565
|
+
process.chdir(previous);
|
|
3566
|
+
}
|
|
3567
|
+
}
|
|
3568
|
+
function releaseSlug(name) {
|
|
3569
|
+
const slug = name.normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3570
|
+
return slug.length > 0 ? slug : "forgeax-game";
|
|
3571
|
+
}
|
|
3572
|
+
async function packageCommand(options = {}) {
|
|
3573
|
+
const facts = await readProjectFacts(options.root);
|
|
3574
|
+
if (!facts.ok) return facts;
|
|
3575
|
+
const built = await buildCommand({
|
|
3576
|
+
root: facts.value.root,
|
|
3577
|
+
base: "./",
|
|
3578
|
+
...options.json === void 0 ? {} : { json: options.json }
|
|
3579
|
+
});
|
|
3580
|
+
if (!built.ok) return built;
|
|
3581
|
+
const distRoot = resolve(facts.value.root, "dist");
|
|
3582
|
+
const verified = await verifyDist(distRoot);
|
|
3583
|
+
if (!verified.ok) return verified;
|
|
3584
|
+
const archive = resolve(
|
|
3585
|
+
facts.value.root,
|
|
3586
|
+
options.output ?? `release/${releaseSlug(facts.value.name)}-web.zip`
|
|
3587
|
+
);
|
|
3588
|
+
const archiveRelativeToDist = relative(distRoot, archive);
|
|
3589
|
+
if (archiveRelativeToDist === "" || !archiveRelativeToDist.startsWith("..")) {
|
|
3590
|
+
return {
|
|
3591
|
+
ok: false,
|
|
3592
|
+
error: {
|
|
3593
|
+
code: "release-output-inside-dist",
|
|
3594
|
+
expected: "the release archive to live outside the derived dist directory",
|
|
3595
|
+
hint: "Use --output release/<game>-web.zip or another path outside dist.",
|
|
3596
|
+
detail: { archive, distRoot }
|
|
3597
|
+
}
|
|
3598
|
+
};
|
|
3599
|
+
}
|
|
3600
|
+
const temporaryArchive = `${archive}.partial-${process.pid}`;
|
|
3601
|
+
const checksumPath = `${archive}.sha256`;
|
|
3602
|
+
const temporaryChecksum = `${checksumPath}.partial-${process.pid}`;
|
|
3603
|
+
try {
|
|
3604
|
+
const paths = [
|
|
3605
|
+
...verified.value.artifacts.map((artifact) => artifact.path),
|
|
3606
|
+
"forgeax-dist.json"
|
|
3607
|
+
];
|
|
3608
|
+
const entries2 = await Promise.all(
|
|
3609
|
+
paths.map(async (path) => ({ path, bytes: await readFile(resolve(distRoot, path)) }))
|
|
3610
|
+
);
|
|
3611
|
+
const bytes = createZip(entries2);
|
|
3612
|
+
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
3613
|
+
const manifestBytes = await readFile(resolve(distRoot, "forgeax-dist.json"));
|
|
3614
|
+
await mkdir(dirname(archive), { recursive: true });
|
|
3615
|
+
await writeFile(temporaryArchive, bytes);
|
|
3616
|
+
await writeFile(temporaryChecksum, `${sha256} ${basename(archive)}
|
|
3617
|
+
`);
|
|
3618
|
+
await rename(temporaryChecksum, checksumPath);
|
|
3619
|
+
await rename(temporaryArchive, archive);
|
|
3620
|
+
return {
|
|
3621
|
+
ok: true,
|
|
3622
|
+
value: {
|
|
3623
|
+
schemaVersion: "1.0.0",
|
|
3624
|
+
format: "forgeax-web-game",
|
|
3625
|
+
target: "web",
|
|
3626
|
+
project: verified.value.project,
|
|
3627
|
+
base: verified.value.base,
|
|
3628
|
+
engine: {
|
|
3629
|
+
delivery: "bundled-runtime",
|
|
3630
|
+
wasm: verified.value.artifacts.some((artifact) => artifact.path.endsWith(".wasm"))
|
|
3631
|
+
},
|
|
3632
|
+
archive: { path: archive, bytes: bytes.byteLength, sha256 },
|
|
3633
|
+
checksumPath,
|
|
3634
|
+
distManifestSha256: createHash("sha256").update(manifestBytes).digest("hex"),
|
|
3635
|
+
run: {
|
|
3636
|
+
local: "forgeax preview",
|
|
3637
|
+
shared: "upload the ZIP to an HTTPS static or HTML-game host and share its URL"
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3640
|
+
};
|
|
3641
|
+
} catch (cause) {
|
|
3642
|
+
return { ok: false, error: commandError(cause, "game-package-failed") };
|
|
3643
|
+
} finally {
|
|
3644
|
+
await Promise.all([
|
|
3645
|
+
rm(temporaryArchive, { force: true }),
|
|
3646
|
+
rm(temporaryChecksum, { force: true })
|
|
3647
|
+
]);
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
async function devCommand(options = {}) {
|
|
3651
|
+
const facts = await readProjectFacts(options.root);
|
|
3652
|
+
if (!facts.ok) return facts;
|
|
3653
|
+
const previous = process.cwd();
|
|
3654
|
+
try {
|
|
3655
|
+
process.chdir(facts.value.root);
|
|
3656
|
+
const server = await createServer(await createViteConfig(facts.value, "serve"));
|
|
3657
|
+
await server.listen();
|
|
3658
|
+
if (options.json !== true) server.printUrls();
|
|
3659
|
+
return { ok: true, value: { root: facts.value.root, urls: server.resolvedUrls } };
|
|
3660
|
+
} catch (cause) {
|
|
3661
|
+
process.chdir(previous);
|
|
3662
|
+
return { ok: false, error: commandError(cause, "dev-server-failed") };
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3665
|
+
async function previewCommand(options = {}) {
|
|
3666
|
+
const root = resolve(options.root ?? process.cwd());
|
|
3667
|
+
const verified = await verifyDist(resolve(root, "dist"));
|
|
3668
|
+
if (!verified.ok) return verified;
|
|
3669
|
+
try {
|
|
3670
|
+
const server = await preview({
|
|
3671
|
+
root,
|
|
3672
|
+
configFile: false,
|
|
3673
|
+
base: verified.value.base,
|
|
3674
|
+
preview: { open: false, port: 0, strictPort: false },
|
|
3675
|
+
build: { outDir: resolve(root, "dist") }
|
|
3676
|
+
});
|
|
3677
|
+
if (options.json !== true) server.printUrls();
|
|
3678
|
+
return { ok: true, value: { root, urls: server.resolvedUrls } };
|
|
3679
|
+
} catch (cause) {
|
|
3680
|
+
return { ok: false, error: commandError(cause, "preview-server-failed") };
|
|
3681
|
+
}
|
|
3682
|
+
}
|
|
3683
|
+
async function testCommand(options = {}) {
|
|
3684
|
+
const facts = await readProjectFacts(options.root);
|
|
3685
|
+
if (!facts.ok) return facts;
|
|
3686
|
+
try {
|
|
3687
|
+
const context = await startVitest("test", [], {
|
|
3688
|
+
root: facts.value.root,
|
|
3689
|
+
run: true,
|
|
3690
|
+
watch: false,
|
|
3691
|
+
passWithNoTests: false
|
|
3692
|
+
});
|
|
3693
|
+
if (context === void 0) {
|
|
3694
|
+
return {
|
|
3695
|
+
ok: false,
|
|
3696
|
+
error: {
|
|
3697
|
+
code: "test-runner-unavailable",
|
|
3698
|
+
expected: "Vitest to create a project test context",
|
|
3699
|
+
hint: "Inspect the project test configuration.",
|
|
3700
|
+
detail: { root: facts.value.root }
|
|
3701
|
+
}
|
|
3702
|
+
};
|
|
3703
|
+
}
|
|
3704
|
+
const failed = context.state.getFiles().filter((file) => file.result?.state === "fail");
|
|
3705
|
+
await context.close();
|
|
3706
|
+
if (failed.length > 0) {
|
|
3707
|
+
return {
|
|
3708
|
+
ok: false,
|
|
3709
|
+
error: {
|
|
3710
|
+
code: "project-tests-failed",
|
|
3711
|
+
expected: "all project tests to pass",
|
|
3712
|
+
hint: "Repair the failing game test before building a release.",
|
|
3713
|
+
detail: { files: failed.map((file) => file.filepath) }
|
|
3714
|
+
}
|
|
3715
|
+
};
|
|
3716
|
+
}
|
|
3717
|
+
return { ok: true, value: { root: facts.value.root } };
|
|
3718
|
+
} catch (cause) {
|
|
3719
|
+
return { ok: false, error: commandError(cause, "project-tests-failed") };
|
|
3720
|
+
}
|
|
3721
|
+
}
|
|
3722
|
+
var init_commands = __esm({
|
|
3723
|
+
"src/commands.ts"() {
|
|
3724
|
+
init_archive();
|
|
3725
|
+
init_dist();
|
|
3726
|
+
init_host();
|
|
3727
|
+
init_project();
|
|
3728
|
+
init_assets();
|
|
3729
|
+
init_bootstrap_commands();
|
|
3730
|
+
init_plugin_authoring();
|
|
3731
|
+
init_cli_context();
|
|
3732
|
+
init_operations();
|
|
3733
|
+
init_sdk_install();
|
|
3734
|
+
init_shader_check();
|
|
3735
|
+
init_skill_install();
|
|
3736
|
+
init_operations();
|
|
3737
|
+
}
|
|
3738
|
+
});
|
|
3739
|
+
var nativePreviewPlugins, nativePreviewTools, nativePreviewDescriptors;
|
|
3740
|
+
var init_preview_catalog = __esm({
|
|
3741
|
+
"src/tools/preview-catalog.ts"() {
|
|
3742
|
+
nativePreviewPlugins = [
|
|
3743
|
+
["@forgeax/engine-preview/material", materialPreviewPlugin],
|
|
3744
|
+
["@forgeax/engine-preview/mesh", meshPreviewPlugin],
|
|
3745
|
+
["@forgeax/engine-preview/vfx", vfxPreviewPlugin],
|
|
3746
|
+
["@forgeax/engine-preview/texture", texturePreviewPlugin]
|
|
3747
|
+
];
|
|
3748
|
+
nativePreviewTools = nativePreviewPlugins.flatMap(([, plugin]) => plugin.tools);
|
|
3749
|
+
nativePreviewDescriptors = nativePreviewTools.map(
|
|
3750
|
+
({ descriptor }) => descriptor
|
|
3751
|
+
);
|
|
3752
|
+
}
|
|
3753
|
+
});
|
|
3754
|
+
function projectDescriptor(descriptor) {
|
|
3755
|
+
return {
|
|
3756
|
+
id: descriptor.id,
|
|
3757
|
+
title: descriptor.title,
|
|
3758
|
+
summary: descriptor.summary,
|
|
3759
|
+
realm: descriptor.realm,
|
|
3760
|
+
evidence: [...descriptor.evidence],
|
|
3761
|
+
...descriptor.argsSchema.describe === void 0 ? {} : { argsSchema: descriptor.argsSchema.describe },
|
|
3762
|
+
...descriptor.resultSchema.describe === void 0 ? {} : { resultSchema: descriptor.resultSchema.describe }
|
|
3763
|
+
};
|
|
3764
|
+
}
|
|
3765
|
+
function missingRealmOwner(descriptor, id) {
|
|
3766
|
+
const realm = descriptor?.realm ?? "build";
|
|
3767
|
+
return {
|
|
3768
|
+
outcome: "failed",
|
|
3769
|
+
failure: capabilityUnavailableError(`realm:${realm}:tool:${id}`, realm),
|
|
3770
|
+
artifacts: []
|
|
3771
|
+
};
|
|
3772
|
+
}
|
|
3773
|
+
function createRealmDispatch(contributions, owners) {
|
|
3774
|
+
const contributionById = new Map(
|
|
3775
|
+
contributions.map((contribution) => [contribution.descriptor.id, contribution])
|
|
3776
|
+
);
|
|
3777
|
+
const ownerByRealm = /* @__PURE__ */ new Map();
|
|
3778
|
+
for (const owner of owners) {
|
|
3779
|
+
if (ownerByRealm.has(owner.realm)) throw new TypeError(`Duplicate realm owner: ${owner.realm}`);
|
|
3780
|
+
const invalid = owner.contributions.find(
|
|
3781
|
+
(contribution) => contribution.descriptor.realm !== owner.realm
|
|
3782
|
+
);
|
|
3783
|
+
if (invalid !== void 0) {
|
|
3784
|
+
throw new TypeError(
|
|
3785
|
+
`Tool ${invalid.descriptor.id} declares ${invalid.descriptor.realm} but owner is ${owner.realm}`
|
|
3786
|
+
);
|
|
3787
|
+
}
|
|
3788
|
+
ownerByRealm.set(owner.realm, createToolRuntime(owner.contributions));
|
|
3789
|
+
}
|
|
3790
|
+
return {
|
|
3791
|
+
list: () => contributions.map((contribution) => contribution.descriptor),
|
|
3792
|
+
describe: (id) => contributionById.get(id)?.descriptor,
|
|
3793
|
+
async run(id, args2, options = {}) {
|
|
3794
|
+
const contribution = contributionById.get(id);
|
|
3795
|
+
if (contribution === void 0)
|
|
3796
|
+
return missingRealmOwner(void 0, id);
|
|
3797
|
+
const owner = ownerByRealm.get(contribution.descriptor.realm);
|
|
3798
|
+
if (owner === void 0) return missingRealmOwner(contribution.descriptor, id);
|
|
3799
|
+
const bound = owner.get(id);
|
|
3800
|
+
if (bound === void 0) return missingRealmOwner(contribution.descriptor, id);
|
|
3801
|
+
return await owner.run(bound, args2, options).terminal;
|
|
3802
|
+
}
|
|
3803
|
+
};
|
|
3804
|
+
}
|
|
3805
|
+
function requiredAuthorityDigest(authority) {
|
|
3806
|
+
if (authority.authorityDigest === void 0) {
|
|
3807
|
+
throw new TypeError("Catalog materialization requires an authority digest");
|
|
3808
|
+
}
|
|
3809
|
+
return authority.authorityDigest;
|
|
3810
|
+
}
|
|
3811
|
+
function digestCatalog(authorityDigest, entries2) {
|
|
3812
|
+
return `sha256:${createHash("sha256").update(JSON.stringify({ authorityDigest, entries: entries2 })).digest("hex")}`;
|
|
3813
|
+
}
|
|
3814
|
+
function digestAuthority(bytes) {
|
|
3815
|
+
const hash = createHash("sha256");
|
|
3816
|
+
for (const value of bytes) hash.update(value);
|
|
3817
|
+
return `sha256:${hash.digest("hex")}`;
|
|
3818
|
+
}
|
|
3819
|
+
function projectionValid(catalog, authorityDigest, descriptors = defaultToolDescriptors) {
|
|
3820
|
+
if (catalog === null || typeof catalog !== "object") return false;
|
|
3821
|
+
const value = catalog;
|
|
3822
|
+
const expectedEntries = descriptors.map(projectDescriptor).sort((left, right) => left.id.localeCompare(right.id));
|
|
3823
|
+
return value.schemaVersion === "1.0.0" && value.authorityDigest === authorityDigest && Array.isArray(value.entries) && JSON.stringify(value.entries) === JSON.stringify(expectedEntries) && value.digest === digestCatalog(authorityDigest, value.entries);
|
|
3824
|
+
}
|
|
3825
|
+
function createProjectToolCatalogAuthority(rootInput, descriptors = defaultToolDescriptors) {
|
|
3826
|
+
const root = resolve(rootInput);
|
|
3827
|
+
return {
|
|
3828
|
+
root,
|
|
3829
|
+
projectionPath: resolve(root, ".forgeax/generated/tool-catalog.json"),
|
|
3830
|
+
async read() {
|
|
3831
|
+
const [forge, packageJson] = await Promise.all([
|
|
3832
|
+
readFile(resolve(root, "forge.json")),
|
|
3833
|
+
readFile(resolve(root, "package.json"))
|
|
3834
|
+
]);
|
|
3835
|
+
return { authorityDigest: digestAuthority([forge, packageJson]), descriptors };
|
|
3836
|
+
}
|
|
3837
|
+
};
|
|
3838
|
+
}
|
|
3839
|
+
async function loadToolCatalog(authority, descriptors = defaultToolDescriptors) {
|
|
3840
|
+
const read = authority.read;
|
|
3841
|
+
if (read === void 0) {
|
|
3842
|
+
if (authority.authorityDigest === void 0) {
|
|
3843
|
+
return {
|
|
3844
|
+
ok: false,
|
|
3845
|
+
error: {
|
|
3846
|
+
code: "tool-catalog-authority-unreadable",
|
|
3847
|
+
expected: "an injectable project authority reader",
|
|
3848
|
+
hint: "Create a project authority from a real project root before listing tools.",
|
|
3849
|
+
detail: { reason: "authority reader is missing" }
|
|
3850
|
+
}
|
|
3851
|
+
};
|
|
3852
|
+
}
|
|
3853
|
+
return { ok: true, value: materializeToolDescriptorCatalog(descriptors, authority) };
|
|
3854
|
+
}
|
|
3855
|
+
let snapshot;
|
|
3856
|
+
try {
|
|
3857
|
+
snapshot = await read();
|
|
3858
|
+
} catch (cause) {
|
|
3859
|
+
return {
|
|
3860
|
+
ok: false,
|
|
3861
|
+
error: {
|
|
3862
|
+
code: "tool-catalog-authority-unreadable",
|
|
3863
|
+
expected: "project authority files to be readable",
|
|
3864
|
+
hint: "Repair forge.json and package.json, then retry catalog rebuild.",
|
|
3865
|
+
detail: { reason: cause instanceof Error ? cause.message : String(cause) }
|
|
3866
|
+
}
|
|
3867
|
+
};
|
|
3868
|
+
}
|
|
3869
|
+
const projectionPath = authority.projectionPath;
|
|
3870
|
+
if (projectionPath !== void 0) {
|
|
3871
|
+
try {
|
|
3872
|
+
const current = JSON.parse(await readFile(projectionPath, "utf8"));
|
|
3873
|
+
if (projectionValid(current, snapshot.authorityDigest, snapshot.descriptors)) {
|
|
3874
|
+
return { ok: true, value: current };
|
|
3875
|
+
}
|
|
3876
|
+
} catch {
|
|
3877
|
+
}
|
|
3878
|
+
}
|
|
3879
|
+
const catalog = materializeToolDescriptorCatalog(snapshot.descriptors, {
|
|
3880
|
+
authorityDigest: snapshot.authorityDigest
|
|
3881
|
+
});
|
|
3882
|
+
if (projectionPath !== void 0) {
|
|
3883
|
+
try {
|
|
3884
|
+
await mkdir(resolve(projectionPath, ".."), { recursive: true });
|
|
3885
|
+
await writeFile(projectionPath, `${JSON.stringify(catalog, null, 2)}
|
|
3886
|
+
`, "utf8");
|
|
3887
|
+
} catch (cause) {
|
|
3888
|
+
return {
|
|
3889
|
+
ok: false,
|
|
3890
|
+
error: {
|
|
3891
|
+
code: "tool-catalog-projection-invalid",
|
|
3892
|
+
expected: "the generated catalog projection to be writable",
|
|
3893
|
+
hint: "Restore write access to .forgeax/generated and retry cold rebuild.",
|
|
3894
|
+
detail: { reason: cause instanceof Error ? cause.message : String(cause) }
|
|
3895
|
+
}
|
|
3896
|
+
};
|
|
3897
|
+
}
|
|
3898
|
+
}
|
|
3899
|
+
return { ok: true, value: catalog };
|
|
3900
|
+
}
|
|
3901
|
+
function materializeToolDescriptorCatalog(descriptors, authority) {
|
|
3902
|
+
const entries2 = descriptors.map(projectDescriptor).sort((left, right) => left.id.localeCompare(right.id));
|
|
3903
|
+
const authorityDigest = requiredAuthorityDigest(authority);
|
|
3904
|
+
return {
|
|
3905
|
+
schemaVersion: "1.0.0",
|
|
3906
|
+
authorityDigest,
|
|
3907
|
+
digest: digestCatalog(authorityDigest, entries2),
|
|
3908
|
+
entries: entries2
|
|
3909
|
+
};
|
|
3910
|
+
}
|
|
3911
|
+
function listTools(catalog) {
|
|
3912
|
+
return catalog.entries;
|
|
3913
|
+
}
|
|
3914
|
+
function describeTool(catalog, id) {
|
|
3915
|
+
return catalog.entries.find((entry) => entry.id === id);
|
|
3916
|
+
}
|
|
3917
|
+
var buildArgsSchema, pluginInstallArgsSchema, previewArgsSchema, resultSchema, offlineAnalysisArgsSchema, projectBuildDescriptor, authorPluginInstallDescriptor, previewRunDescriptor, previewOfflineAnalysisDescriptor, defaultToolDescriptors;
|
|
3918
|
+
var init_catalog = __esm({
|
|
3919
|
+
"src/tools/catalog.ts"() {
|
|
3920
|
+
init_preview_catalog();
|
|
3921
|
+
buildArgsSchema = {
|
|
3922
|
+
parse(value) {
|
|
3923
|
+
if (value === null || typeof value !== "object")
|
|
3924
|
+
return { ok: false, error: "expected an options object" };
|
|
3925
|
+
return { ok: true, value };
|
|
3926
|
+
},
|
|
3927
|
+
describe: '{"type":"object","properties":{"root":{"type":"string"},"base":{"type":"string"}}}'
|
|
3928
|
+
};
|
|
3929
|
+
pluginInstallArgsSchema = {
|
|
3930
|
+
parse(value) {
|
|
3931
|
+
if (value === null || typeof value !== "object")
|
|
3932
|
+
return { ok: false, error: "expected a plugin install options object" };
|
|
3933
|
+
return { ok: true, value };
|
|
3934
|
+
},
|
|
3935
|
+
describe: '{"type":"object","required":["id","module"],"properties":{"id":{"type":"string"},"module":{"type":"string"},"realm":{"enum":["host","engine","build"]},"dependency":{"type":"string"},"root":{"type":"string"}}}'
|
|
3936
|
+
};
|
|
3937
|
+
previewArgsSchema = {
|
|
3938
|
+
parse(value) {
|
|
3939
|
+
if (value === null || typeof value !== "object")
|
|
3940
|
+
return { ok: false, error: "expected a preview request object" };
|
|
3941
|
+
const request = value;
|
|
3942
|
+
if (request.recipe === void 0 || typeof request.recipe !== "object")
|
|
3943
|
+
return { ok: false, error: "expected recipe in the preview request" };
|
|
3944
|
+
return { ok: true, value };
|
|
3945
|
+
},
|
|
3946
|
+
describe: '{"type":"object","required":["recipe"],"properties":{"recipe":{"type":"object","required":["snapshot"],"properties":{"snapshot":{"type":"object","required":["revision","digest"]},"backend":{"const":"webgpu"},"presentation":{"enum":["hidden","visible"]},"viewport":{"type":"object","properties":{"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1}}},"actions":{"type":"array","items":{"type":"object","required":["frame","name"],"properties":{"frame":{"type":"integer","minimum":0},"name":{"type":"string","minLength":1},"value":{}}}},"deltaSeconds":{"type":"number","minimum":0},"frames":{"type":"integer","minimum":1}}}}}'
|
|
3947
|
+
};
|
|
3948
|
+
resultSchema = {
|
|
3949
|
+
parse: (value) => ({ ok: true, value }),
|
|
3950
|
+
describe: '{"type":"object"}'
|
|
3951
|
+
};
|
|
3952
|
+
offlineAnalysisArgsSchema = {
|
|
3953
|
+
parse(value) {
|
|
3954
|
+
if (value === null || typeof value !== "object")
|
|
3955
|
+
return { ok: false, error: "expected an offline analysis request object" };
|
|
3956
|
+
const request = value;
|
|
3957
|
+
if (request.manifest === void 0 || typeof request.manifest !== "object")
|
|
3958
|
+
return { ok: false, error: "expected manifest in the offline analysis request" };
|
|
3959
|
+
return { ok: true, value };
|
|
3960
|
+
},
|
|
3961
|
+
describe: '{"type":"object","required":["manifest"],"properties":{"manifest":{"type":"object","required":["schemaVersion","identity","artifacts"]},"required":{"type":"array","items":{"enum":["rhi-tape","png","profile-capture"]}}}}'
|
|
3962
|
+
};
|
|
3963
|
+
projectBuildDescriptor = {
|
|
3964
|
+
id: "project.build",
|
|
3965
|
+
title: "Build project",
|
|
3966
|
+
summary: "Builds the project through the existing Vite and Pack authorities.",
|
|
3967
|
+
realm: "build",
|
|
3968
|
+
argsSchema: buildArgsSchema,
|
|
3969
|
+
resultSchema,
|
|
3970
|
+
evidence: []
|
|
3971
|
+
};
|
|
3972
|
+
authorPluginInstallDescriptor = {
|
|
3973
|
+
id: "author.plugin-install",
|
|
3974
|
+
title: "Install project plugin",
|
|
3975
|
+
summary: "Writes one plugin Entry through the project authoring authority.",
|
|
3976
|
+
realm: "build",
|
|
3977
|
+
argsSchema: pluginInstallArgsSchema,
|
|
3978
|
+
resultSchema,
|
|
3979
|
+
evidence: []
|
|
3980
|
+
};
|
|
3981
|
+
previewRunDescriptor = {
|
|
3982
|
+
id: "preview.run",
|
|
3983
|
+
title: "Run hidden WebGPU preview",
|
|
3984
|
+
summary: "Runs one fixed real-WebGPU preview recipe and returns structured evidence refs.",
|
|
3985
|
+
realm: "host",
|
|
3986
|
+
argsSchema: previewArgsSchema,
|
|
3987
|
+
resultSchema,
|
|
3988
|
+
evidence: ["rhi-tape", "png", "profile-capture"]
|
|
3989
|
+
};
|
|
3990
|
+
previewOfflineAnalysisDescriptor = {
|
|
3991
|
+
id: "preview.offline-analysis",
|
|
3992
|
+
title: "Analyze preview artifacts",
|
|
3993
|
+
summary: "Validates owner-separated evidence identity before offline consumption.",
|
|
3994
|
+
realm: "build",
|
|
3995
|
+
argsSchema: offlineAnalysisArgsSchema,
|
|
3996
|
+
resultSchema,
|
|
3997
|
+
evidence: ["rhi-tape", "png", "profile-capture"]
|
|
3998
|
+
};
|
|
3999
|
+
defaultToolDescriptors = [
|
|
4000
|
+
projectBuildDescriptor,
|
|
4001
|
+
authorPluginInstallDescriptor,
|
|
4002
|
+
...nativePreviewDescriptors
|
|
4003
|
+
];
|
|
4004
|
+
}
|
|
4005
|
+
});
|
|
4006
|
+
function retiredPreviewTool() {
|
|
4007
|
+
return {
|
|
4008
|
+
outcome: "failed",
|
|
4009
|
+
failure: domainFailureError(
|
|
4010
|
+
retiredPreviewOperationFailure.code,
|
|
4011
|
+
retiredPreviewOperationFailure.expected,
|
|
4012
|
+
retiredPreviewOperationFailure.hint,
|
|
4013
|
+
retiredPreviewOperationFailure.detail
|
|
4014
|
+
),
|
|
4015
|
+
artifacts: []
|
|
4016
|
+
};
|
|
4017
|
+
}
|
|
4018
|
+
var retiredPreviewOperationFailure;
|
|
4019
|
+
var init_preview_migration = __esm({
|
|
4020
|
+
"src/tools/preview-migration.ts"() {
|
|
4021
|
+
retiredPreviewOperationFailure = {
|
|
4022
|
+
code: "preview-operation-migrated",
|
|
4023
|
+
expected: "a domain-specific preview contribution selected from the project catalog",
|
|
4024
|
+
hint: "Use material.preview, mesh.preview, vfx.preview, or texture.preview with a Project GUID.",
|
|
4025
|
+
detail: {
|
|
4026
|
+
operation: "preview.run",
|
|
4027
|
+
replacements: ["material.preview", "mesh.preview", "vfx.preview", "texture.preview"]
|
|
4028
|
+
}
|
|
4029
|
+
};
|
|
4030
|
+
}
|
|
4031
|
+
});
|
|
4032
|
+
|
|
4033
|
+
// src/tools/project-tools.ts
|
|
4034
|
+
var project_tools_exports = {};
|
|
4035
|
+
__export(project_tools_exports, {
|
|
4036
|
+
discoverProjectTools: () => discoverProjectTools,
|
|
4037
|
+
runProjectTool: () => runProjectTool
|
|
4038
|
+
});
|
|
4039
|
+
function projectLeaves(entries2, inheritedRealm = "engine") {
|
|
4040
|
+
const leaves = [];
|
|
4041
|
+
for (const entry of entries2) {
|
|
4042
|
+
const realm = entry.realm ?? inheritedRealm;
|
|
4043
|
+
if (entry.disabled === true) continue;
|
|
4044
|
+
if (entry.group === true) {
|
|
4045
|
+
leaves.push(...projectLeaves(entry.config, realm));
|
|
4046
|
+
continue;
|
|
4047
|
+
}
|
|
4048
|
+
if (!entry.name.startsWith("cordis:")) {
|
|
4049
|
+
leaves.push({ entry, moduleName: entry.name, realm });
|
|
4050
|
+
}
|
|
4051
|
+
}
|
|
4052
|
+
return leaves;
|
|
4053
|
+
}
|
|
4054
|
+
function exportedToolPlugin(module) {
|
|
4055
|
+
if (isToolPlugin(module)) return module;
|
|
4056
|
+
if (typeof module !== "object" || module === null) return void 0;
|
|
4057
|
+
const value = Reflect.get(module, "default");
|
|
4058
|
+
return isToolPlugin(value) ? value : void 0;
|
|
4059
|
+
}
|
|
4060
|
+
function staticContributions(entry) {
|
|
4061
|
+
if (entry.config === null || typeof entry.config !== "object") return [];
|
|
4062
|
+
const tools = Reflect.get(entry.config, "tools");
|
|
4063
|
+
if (!Array.isArray(tools)) return [];
|
|
4064
|
+
return tools.flatMap((candidate) => {
|
|
4065
|
+
if (candidate === null || typeof candidate !== "object") return [];
|
|
4066
|
+
const declaration = candidate;
|
|
4067
|
+
if (typeof declaration.id !== "string" || typeof declaration.title !== "string" || typeof declaration.summary !== "string" || declaration.realm !== "build" && declaration.realm !== "host" && declaration.realm !== "engine")
|
|
4068
|
+
return [];
|
|
4069
|
+
const descriptor = {
|
|
4070
|
+
id: declaration.id,
|
|
4071
|
+
title: declaration.title,
|
|
4072
|
+
summary: declaration.summary,
|
|
4073
|
+
realm: declaration.realm,
|
|
4074
|
+
argsSchema: {
|
|
4075
|
+
parse: (value) => ({ ok: true, value }),
|
|
4076
|
+
...declaration.argsSchema === void 0 ? {} : { describe: declaration.argsSchema }
|
|
4077
|
+
},
|
|
4078
|
+
resultSchema: {
|
|
4079
|
+
parse: (value) => ({ ok: true, value }),
|
|
4080
|
+
...declaration.resultSchema === void 0 ? {} : { describe: declaration.resultSchema }
|
|
4081
|
+
},
|
|
4082
|
+
evidence: declaration.evidence ?? []
|
|
4083
|
+
};
|
|
4084
|
+
return [
|
|
4085
|
+
{
|
|
4086
|
+
descriptor,
|
|
4087
|
+
execute: async () => ({
|
|
4088
|
+
ok: false,
|
|
4089
|
+
error: { code: "tool-static-descriptor", detail: {} }
|
|
4090
|
+
})
|
|
4091
|
+
}
|
|
4092
|
+
];
|
|
4093
|
+
});
|
|
4094
|
+
}
|
|
4095
|
+
function hasStaticToolDeclarations(entry) {
|
|
4096
|
+
if (entry.config === null || typeof entry.config !== "object") return false;
|
|
4097
|
+
return Array.isArray(Reflect.get(entry.config, "tools"));
|
|
4098
|
+
}
|
|
4099
|
+
async function createViteModuleLoader(root) {
|
|
4100
|
+
const server = await createServer({
|
|
4101
|
+
root,
|
|
4102
|
+
appType: "custom",
|
|
4103
|
+
configFile: false,
|
|
4104
|
+
logLevel: "silent",
|
|
4105
|
+
server: { middlewareMode: true }
|
|
4106
|
+
});
|
|
4107
|
+
return {
|
|
4108
|
+
load(name) {
|
|
4109
|
+
const id = name.startsWith(".") ? `/@fs/${resolve(root, name)}` : name;
|
|
4110
|
+
return server.ssrLoadModule(id);
|
|
4111
|
+
},
|
|
4112
|
+
close: () => server.close()
|
|
4113
|
+
};
|
|
4114
|
+
}
|
|
4115
|
+
async function readProjectEntries(root) {
|
|
4116
|
+
const raw = JSON.parse(await readFile(resolve(root, "forge.json"), "utf8"));
|
|
4117
|
+
const parsed = GameProjectSchema.safeParse(raw);
|
|
4118
|
+
if (!parsed.success) {
|
|
4119
|
+
throw new TypeError(`Invalid forge.json: ${parsed.error.message}`);
|
|
4120
|
+
}
|
|
4121
|
+
return parsed.data.plugins ?? [];
|
|
4122
|
+
}
|
|
4123
|
+
async function discoverProjectTools(rootInput, options = {}) {
|
|
4124
|
+
const root = resolve(rootInput);
|
|
4125
|
+
const loadModules = options.loadModules ?? true;
|
|
4126
|
+
const loader = loadModules ? options.moduleLoader ?? await createViteModuleLoader(root) : void 0;
|
|
4127
|
+
try {
|
|
4128
|
+
const bindings = [];
|
|
4129
|
+
const ids = /* @__PURE__ */ new Map();
|
|
4130
|
+
for (const leaf of projectLeaves(await readProjectEntries(root))) {
|
|
4131
|
+
if (leaf.realm !== "build" && leaf.realm !== "host" && leaf.realm !== "engine") continue;
|
|
4132
|
+
if (leaf.realm === "engine" && !hasStaticToolDeclarations(leaf.entry)) continue;
|
|
4133
|
+
const toolPlugin = loadModules ? exportedToolPlugin(await loader.load(leaf.moduleName)) : void 0;
|
|
4134
|
+
const contributions = toolPlugin?.tools ?? staticContributions(leaf.entry);
|
|
4135
|
+
for (const contribution of contributions) {
|
|
4136
|
+
if (contribution.descriptor.realm !== leaf.realm) {
|
|
4137
|
+
throw new TypeError(
|
|
4138
|
+
`Tool ${contribution.descriptor.id} declares ${contribution.descriptor.realm} but Entry ${leaf.entry.id} owns ${leaf.realm}`
|
|
4139
|
+
);
|
|
4140
|
+
}
|
|
4141
|
+
const existing = ids.get(contribution.descriptor.id);
|
|
4142
|
+
if (existing !== void 0) {
|
|
4143
|
+
throw new TypeError(
|
|
4144
|
+
`Duplicate project tool id ${contribution.descriptor.id} from ${existing} and ${leaf.entry.id}`
|
|
4145
|
+
);
|
|
4146
|
+
}
|
|
4147
|
+
ids.set(contribution.descriptor.id, leaf.entry.id);
|
|
4148
|
+
bindings.push({
|
|
4149
|
+
contribution,
|
|
4150
|
+
entry: leaf.entry,
|
|
4151
|
+
moduleName: leaf.moduleName,
|
|
4152
|
+
realm: leaf.realm,
|
|
4153
|
+
...toolPlugin === void 0 ? {} : { toolPlugin },
|
|
4154
|
+
loadToolPlugin: async () => {
|
|
4155
|
+
if (toolPlugin !== void 0) return toolPlugin;
|
|
4156
|
+
const selectedLoader = options.moduleLoader ?? await createViteModuleLoader(root);
|
|
4157
|
+
try {
|
|
4158
|
+
const loaded = exportedToolPlugin(await selectedLoader.load(leaf.moduleName));
|
|
4159
|
+
if (loaded === void 0)
|
|
4160
|
+
throw new TypeError(`Entry ${leaf.entry.id} does not export a ToolPlugin`);
|
|
4161
|
+
return loaded;
|
|
4162
|
+
} finally {
|
|
4163
|
+
await selectedLoader.close();
|
|
4164
|
+
}
|
|
4165
|
+
}
|
|
4166
|
+
});
|
|
4167
|
+
}
|
|
4168
|
+
}
|
|
4169
|
+
return bindings;
|
|
4170
|
+
} finally {
|
|
4171
|
+
if (loader !== void 0) await loader.close();
|
|
4172
|
+
}
|
|
4173
|
+
}
|
|
4174
|
+
function activationFailure(cause) {
|
|
4175
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
4176
|
+
return {
|
|
4177
|
+
outcome: "failed",
|
|
4178
|
+
failure: {
|
|
4179
|
+
code: "tool-domain-failed",
|
|
4180
|
+
expected: "the owning project plugin Fiber to become ready",
|
|
4181
|
+
hint: "Repair the project Entry, plugin dependencies, or plugin apply failure before retrying.",
|
|
4182
|
+
detail: { code: "tool-plugin-activation-failed", payload: message }
|
|
4183
|
+
},
|
|
4184
|
+
artifacts: []
|
|
4185
|
+
};
|
|
4186
|
+
}
|
|
4187
|
+
async function runProjectTool(binding, args2, options) {
|
|
4188
|
+
const ctx = new Context();
|
|
4189
|
+
try {
|
|
4190
|
+
const toolPlugin = await binding.loadToolPlugin();
|
|
4191
|
+
const contribution = toolPlugin.tools.find(
|
|
4192
|
+
(candidate) => candidate.descriptor.id === binding.contribution.descriptor.id
|
|
4193
|
+
);
|
|
4194
|
+
if (contribution === void 0)
|
|
4195
|
+
return activationFailure(new Error("selected tool is absent from Entry"));
|
|
4196
|
+
const catalog = /* @__PURE__ */ new Map([
|
|
4197
|
+
[
|
|
4198
|
+
binding.moduleName,
|
|
4199
|
+
{
|
|
4200
|
+
realm: binding.realm,
|
|
4201
|
+
load: async () => ({ default: toolPlugin })
|
|
4202
|
+
}
|
|
4203
|
+
]
|
|
4204
|
+
]);
|
|
4205
|
+
const { loader } = await installCatalogLoader(ctx, catalog, binding.realm);
|
|
4206
|
+
const entries2 = projectPluginEntries(
|
|
4207
|
+
[binding.entry],
|
|
4208
|
+
binding.realm,
|
|
4209
|
+
binding.realm
|
|
4210
|
+
);
|
|
4211
|
+
await loader.root.update(entries2);
|
|
4212
|
+
await loader.await();
|
|
4213
|
+
const { createToolRuntime: createToolRuntime4 } = await import('@forgeax/engine-tool-runtime');
|
|
4214
|
+
const { createContextCapabilityResolver } = await import('@forgeax/engine-plugin');
|
|
4215
|
+
return await createToolRuntime4([contribution]).run(contribution, args2, {
|
|
4216
|
+
...options,
|
|
4217
|
+
capabilityResolver: createContextCapabilityResolver(ctx)
|
|
4218
|
+
}).terminal;
|
|
4219
|
+
} catch (cause) {
|
|
4220
|
+
return activationFailure(cause);
|
|
4221
|
+
} finally {
|
|
4222
|
+
await ctx.fiber.dispose();
|
|
4223
|
+
}
|
|
4224
|
+
}
|
|
4225
|
+
var init_project_tools = __esm({
|
|
4226
|
+
"src/tools/project-tools.ts"() {
|
|
4227
|
+
}
|
|
4228
|
+
});
|
|
4229
|
+
function analyzePreviewArtifacts(request) {
|
|
4230
|
+
const required = request.required ?? [];
|
|
4231
|
+
const validated = request.manifest.schemaVersion === "1.0.0" ? required.some((kind) => kind !== "rhi-tape" && kind !== "png" && kind !== "profile-capture") ? {
|
|
4232
|
+
ok: false,
|
|
4233
|
+
error: {
|
|
4234
|
+
code: "tool-artifact-manifest-invalid",
|
|
4235
|
+
expected: "requested evidence kinds to belong to the v1 artifact manifest",
|
|
4236
|
+
hint: "Request one of rhi-tape, png, or profile-capture for a v1 manifest.",
|
|
4237
|
+
detail: { reason: "unsupported required evidence kind" }
|
|
4238
|
+
}
|
|
4239
|
+
} : validateArtifactManifest(request.manifest, required) : validatePreviewArtifactManifest(
|
|
4240
|
+
request.manifest,
|
|
4241
|
+
(request.required ?? []).filter(
|
|
4242
|
+
(role) => role === "report" || role === "rhi-tape" || role === "capture" || role === "fresh-replay" || role === "profile-capture" || role === "contact-sheet"
|
|
4243
|
+
)
|
|
4244
|
+
);
|
|
4245
|
+
if (!validated.ok) return { ok: false, error: validated.error };
|
|
4246
|
+
return {
|
|
4247
|
+
ok: true,
|
|
4248
|
+
value: {
|
|
4249
|
+
runId: request.manifest.identity.runId,
|
|
4250
|
+
captureId: request.manifest.identity.captureId,
|
|
4251
|
+
artifacts: request.manifest.artifacts.map(({ kind, digest }) => ({ kind, digest }))
|
|
4252
|
+
}
|
|
4253
|
+
};
|
|
4254
|
+
}
|
|
4255
|
+
var init_offline_analysis = __esm({
|
|
4256
|
+
"src/tools/offline-analysis.ts"() {
|
|
4257
|
+
}
|
|
4258
|
+
});
|
|
4259
|
+
function createPreviewContribution() {
|
|
4260
|
+
return defineTool(previewRunDescriptor, async () => ({
|
|
4261
|
+
ok: false,
|
|
4262
|
+
error: retiredPreviewOperationFailure
|
|
4263
|
+
}));
|
|
4264
|
+
}
|
|
4265
|
+
function createOfflineAnalysisContribution() {
|
|
4266
|
+
return defineTool(
|
|
4267
|
+
{
|
|
4268
|
+
...previewOfflineAnalysisDescriptor
|
|
4269
|
+
},
|
|
4270
|
+
(request) => analyzePreviewArtifacts(request)
|
|
4271
|
+
);
|
|
4272
|
+
}
|
|
4273
|
+
function createPreviewContributions() {
|
|
4274
|
+
return [
|
|
4275
|
+
createPreviewContribution(),
|
|
4276
|
+
createOfflineAnalysisContribution()
|
|
4277
|
+
];
|
|
4278
|
+
}
|
|
4279
|
+
function createDomainPreviewContributions() {
|
|
4280
|
+
return [
|
|
4281
|
+
createMaterialPreviewContribution(),
|
|
4282
|
+
createMeshPreviewContribution(),
|
|
4283
|
+
createVfxPreviewContribution(),
|
|
4284
|
+
createTexturePreviewContribution()
|
|
4285
|
+
];
|
|
4286
|
+
}
|
|
4287
|
+
var init_preview_contributions = __esm({
|
|
4288
|
+
"src/tools/preview-contributions.ts"() {
|
|
4289
|
+
init_catalog();
|
|
4290
|
+
init_offline_analysis();
|
|
4291
|
+
init_preview_migration();
|
|
4292
|
+
}
|
|
4293
|
+
});
|
|
4294
|
+
|
|
4295
|
+
// src/tools/contributions.ts
|
|
4296
|
+
var contributions_exports = {};
|
|
4297
|
+
__export(contributions_exports, {
|
|
4298
|
+
createAuthorContribution: () => createAuthorContribution,
|
|
4299
|
+
createBuildContribution: () => createBuildContribution,
|
|
4300
|
+
createDefaultContributions: () => createDefaultContributions
|
|
4301
|
+
});
|
|
4302
|
+
function commandFailure(error) {
|
|
4303
|
+
return { ok: false, error: { ...error, detail: error.detail } };
|
|
4304
|
+
}
|
|
4305
|
+
function createBuildContribution(projectRoot2 = process.cwd()) {
|
|
4306
|
+
return defineTool(
|
|
4307
|
+
projectBuildDescriptor,
|
|
4308
|
+
async (options) => {
|
|
4309
|
+
const { buildCommand: buildCommand2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
4310
|
+
const result = await buildCommand2({ ...options, root: options.root ?? projectRoot2 });
|
|
4311
|
+
return result.ok ? result.value : commandFailure(result.error);
|
|
4312
|
+
}
|
|
4313
|
+
);
|
|
4314
|
+
}
|
|
4315
|
+
function createAuthorContribution(projectRoot2 = process.cwd()) {
|
|
4316
|
+
return defineTool(
|
|
4317
|
+
authorPluginInstallDescriptor,
|
|
4318
|
+
async (options) => {
|
|
4319
|
+
const { pluginInstallCommand: pluginInstallCommand2 } = await Promise.resolve().then(() => (init_plugin_authoring(), plugin_authoring_exports));
|
|
4320
|
+
const result = await pluginInstallCommand2({ ...options, root: options.root ?? projectRoot2 });
|
|
4321
|
+
return result.ok ? result.value : commandFailure(result.error);
|
|
4322
|
+
}
|
|
4323
|
+
);
|
|
4324
|
+
}
|
|
4325
|
+
function createDefaultContributions(projectRoot2 = process.cwd()) {
|
|
4326
|
+
return [
|
|
4327
|
+
createBuildContribution(projectRoot2),
|
|
4328
|
+
createAuthorContribution(projectRoot2),
|
|
4329
|
+
...createDomainPreviewContributions()
|
|
4330
|
+
];
|
|
4331
|
+
}
|
|
4332
|
+
var init_contributions = __esm({
|
|
4333
|
+
"src/tools/contributions.ts"() {
|
|
4334
|
+
init_catalog();
|
|
4335
|
+
init_preview_contributions();
|
|
4336
|
+
}
|
|
4337
|
+
});
|
|
4338
|
+
|
|
4339
|
+
// src/tools/runtime.ts
|
|
4340
|
+
var runtime_exports = {};
|
|
4341
|
+
__export(runtime_exports, {
|
|
4342
|
+
createDevkitToolRuntime: () => createDevkitToolRuntime,
|
|
4343
|
+
createPreviewToolRuntime: () => createPreviewToolRuntime
|
|
4344
|
+
});
|
|
4345
|
+
function createDevkitToolRuntime(contributions) {
|
|
4346
|
+
return createToolRuntime(contributions);
|
|
4347
|
+
}
|
|
4348
|
+
function createPreviewToolRuntime() {
|
|
4349
|
+
return createToolRuntime(createPreviewContributions());
|
|
4350
|
+
}
|
|
4351
|
+
var init_runtime = __esm({
|
|
4352
|
+
"src/tools/runtime.ts"() {
|
|
4353
|
+
init_preview_contributions();
|
|
4354
|
+
}
|
|
4355
|
+
});
|
|
4356
|
+
|
|
4357
|
+
// src/cli.ts
|
|
4358
|
+
init_commands();
|
|
4359
|
+
|
|
4360
|
+
// src/tools/client.ts
|
|
4361
|
+
init_catalog();
|
|
4362
|
+
function decorateResourcePreviewTerminal(terminal) {
|
|
4363
|
+
if (terminal.outcome === "succeeded") return terminal;
|
|
4364
|
+
const recovery = describeResourcePreviewFailure(terminal.failure);
|
|
4365
|
+
if (recovery === void 0) return terminal;
|
|
4366
|
+
if (terminal.failure.code !== "tool-domain-failed") return terminal;
|
|
4367
|
+
return {
|
|
4368
|
+
...terminal,
|
|
4369
|
+
failure: {
|
|
4370
|
+
...terminal.failure,
|
|
4371
|
+
detail: {
|
|
4372
|
+
...terminal.failure.detail,
|
|
4373
|
+
...recovery.suggestedOperation === void 0 ? {} : { suggestedOperation: recovery.suggestedOperation },
|
|
4374
|
+
recovery: recovery.actions
|
|
4375
|
+
}
|
|
4376
|
+
}
|
|
4377
|
+
};
|
|
4378
|
+
}
|
|
4379
|
+
|
|
4380
|
+
// src/tools/client.ts
|
|
4381
|
+
init_preview_migration();
|
|
4382
|
+
function missingTool(id) {
|
|
4383
|
+
return {
|
|
4384
|
+
outcome: "failed",
|
|
4385
|
+
failure: {
|
|
4386
|
+
code: "tool-capability-unavailable",
|
|
4387
|
+
expected: `tool ${id} to exist in the project-derived catalog`,
|
|
4388
|
+
hint: "Run tool list and choose one of the discovered operation ids.",
|
|
4389
|
+
detail: { capability: `tool:${id}`, realm: "build" }
|
|
4390
|
+
},
|
|
4391
|
+
artifacts: []
|
|
4392
|
+
};
|
|
4393
|
+
}
|
|
4394
|
+
async function createToolClient(options) {
|
|
4395
|
+
const projectDiscovery = options.projectDiscovery;
|
|
4396
|
+
const runProjectTool2 = projectDiscovery === void 0 ? (await Promise.resolve().then(() => (init_project_tools(), project_tools_exports))).runProjectTool : void 0;
|
|
4397
|
+
const builtins = options.baseContributions ?? (await Promise.resolve().then(() => (init_contributions(), contributions_exports))).createDefaultContributions(options.projectRoot);
|
|
4398
|
+
const project = projectDiscovery === void 0 ? await (await Promise.resolve().then(() => (init_project_tools(), project_tools_exports))).discoverProjectTools(
|
|
4399
|
+
options.projectRoot,
|
|
4400
|
+
options
|
|
4401
|
+
) : await projectDiscovery(options.projectRoot);
|
|
4402
|
+
const contributions = [...builtins, ...project.map(({ contribution }) => contribution)];
|
|
4403
|
+
const runtime = options.baseContributions === void 0 ? (await Promise.resolve().then(() => (init_runtime(), runtime_exports))).createDevkitToolRuntime(contributions) : createToolRuntime(contributions);
|
|
4404
|
+
const realmDispatch = options.realmOwners === void 0 ? void 0 : createRealmDispatch(contributions, options.realmOwners);
|
|
4405
|
+
const bindingById = new Map(
|
|
4406
|
+
project.map((binding) => [binding.contribution.descriptor.id, binding])
|
|
4407
|
+
);
|
|
4408
|
+
const descriptors = runtime.list();
|
|
4409
|
+
const loaded = await loadToolCatalog(
|
|
4410
|
+
createProjectToolCatalogAuthority(options.projectRoot, descriptors),
|
|
4411
|
+
descriptors
|
|
4412
|
+
);
|
|
4413
|
+
if (!loaded.ok) throw loaded.error;
|
|
4414
|
+
return {
|
|
4415
|
+
list: () => listTools(loaded.value),
|
|
4416
|
+
describe: (id) => describeTool(loaded.value, id),
|
|
4417
|
+
async run(id, args2, runOptions = {}) {
|
|
4418
|
+
if (id === "preview.run") return retiredPreviewTool();
|
|
4419
|
+
const contribution = runtime.get(id);
|
|
4420
|
+
if (contribution === void 0) return missingTool(id);
|
|
4421
|
+
const binding = bindingById.get(id);
|
|
4422
|
+
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);
|
|
4423
|
+
return decorateResourcePreviewTerminal(terminal);
|
|
4424
|
+
}
|
|
4425
|
+
};
|
|
4426
|
+
}
|
|
4427
|
+
|
|
4428
|
+
// src/tools/commands.ts
|
|
4429
|
+
init_preview_migration();
|
|
4430
|
+
|
|
4431
|
+
// src/tools/run-command.ts
|
|
4432
|
+
async function runOperationCommand(options) {
|
|
4433
|
+
const client = await createToolClient({ projectRoot: options.root ?? process.cwd() });
|
|
4434
|
+
return client.run(options.id, JSON.parse(options.args)).then(decorateResourcePreviewTerminal);
|
|
4435
|
+
}
|
|
4436
|
+
|
|
4437
|
+
// src/tools/commands.ts
|
|
4438
|
+
async function commandClient(root) {
|
|
4439
|
+
try {
|
|
4440
|
+
return { ok: true, value: await createToolClient({ projectRoot: root }) };
|
|
4441
|
+
} catch (cause) {
|
|
4442
|
+
return {
|
|
4443
|
+
ok: false,
|
|
4444
|
+
error: {
|
|
4445
|
+
code: "tool-catalog-authority-unreadable",
|
|
4446
|
+
expected: "project plugin modules and tool descriptors to load from the project authority",
|
|
4447
|
+
hint: "Repair forge.json, plugin module exports, or duplicate tool ids before retrying.",
|
|
4448
|
+
detail: { reason: cause instanceof Error ? cause.message : String(cause) }
|
|
4449
|
+
}
|
|
4450
|
+
};
|
|
4451
|
+
}
|
|
4452
|
+
}
|
|
4453
|
+
async function listCommand(root = process.cwd()) {
|
|
4454
|
+
const client = await commandClient(root);
|
|
4455
|
+
return client.ok ? { ok: true, value: client.value.list() } : client;
|
|
4456
|
+
}
|
|
4457
|
+
async function describeCommand(options) {
|
|
4458
|
+
if (options.id === void 0) {
|
|
4459
|
+
return {
|
|
4460
|
+
ok: false,
|
|
4461
|
+
error: {
|
|
4462
|
+
code: "cli-parse-error",
|
|
4463
|
+
expected: "forgeax describe <operation-id>",
|
|
4464
|
+
hint: "Pass the stable id returned by forgeax list.",
|
|
4465
|
+
detail: {}
|
|
4466
|
+
}
|
|
4467
|
+
};
|
|
4468
|
+
}
|
|
4469
|
+
const client = await commandClient(options.root ?? process.cwd());
|
|
4470
|
+
if (!client.ok) return client;
|
|
4471
|
+
const entry = client.value.describe(options.id);
|
|
4472
|
+
return entry === void 0 ? {
|
|
4473
|
+
ok: false,
|
|
4474
|
+
error: {
|
|
4475
|
+
code: "tool-not-found",
|
|
4476
|
+
expected: "the requested operation id to exist in the static catalog",
|
|
4477
|
+
hint: "Run forgeax list and choose one of its ids.",
|
|
4478
|
+
detail: { id: options.id }
|
|
4479
|
+
}
|
|
4480
|
+
} : { ok: true, value: entry };
|
|
4481
|
+
}
|
|
4482
|
+
async function runCommand(options) {
|
|
4483
|
+
if (options.id === "preview.run") return { ok: true, value: retiredPreviewTool() };
|
|
4484
|
+
if (options.id === void 0 || options.input === void 0 && options.args === void 0) {
|
|
4485
|
+
return {
|
|
4486
|
+
ok: false,
|
|
4487
|
+
error: {
|
|
4488
|
+
code: "cli-parse-error",
|
|
4489
|
+
expected: "forgeax run <operation-id> --input <request.json>",
|
|
4490
|
+
hint: "Describe the operation first, then pass a JSON file matching its args schema.",
|
|
4491
|
+
detail: {}
|
|
4492
|
+
}
|
|
4493
|
+
};
|
|
4494
|
+
}
|
|
4495
|
+
let encoded = options.args;
|
|
4496
|
+
try {
|
|
4497
|
+
const input = options.input;
|
|
4498
|
+
encoded = encoded ?? (input === "-" ? readFileSync(0, "utf8") : await readFile(resolve(options.root ?? process.cwd(), input), "utf8"));
|
|
4499
|
+
JSON.parse(encoded);
|
|
4500
|
+
} catch {
|
|
4501
|
+
return {
|
|
4502
|
+
ok: true,
|
|
4503
|
+
value: {
|
|
4504
|
+
outcome: "failed",
|
|
4505
|
+
failure: {
|
|
4506
|
+
code: "tool-invalid-args",
|
|
4507
|
+
expected: "operation arguments to be valid JSON",
|
|
4508
|
+
hint: "Encode one JSON value that conforms to the descriptor argsSchema.",
|
|
4509
|
+
detail: { message: "Invalid JSON", value: null }
|
|
4510
|
+
},
|
|
4511
|
+
artifacts: []
|
|
4512
|
+
}
|
|
4513
|
+
};
|
|
4514
|
+
}
|
|
4515
|
+
return { ok: true, value: await runOperationCommand({ ...options, args: encoded }) };
|
|
4516
|
+
}
|
|
4517
|
+
async function execCommand(options) {
|
|
4518
|
+
if (options.program === void 0) {
|
|
4519
|
+
return {
|
|
4520
|
+
ok: false,
|
|
4521
|
+
error: {
|
|
4522
|
+
code: "cli-parse-error",
|
|
4523
|
+
expected: "forgeax exec <program.mjs>",
|
|
4524
|
+
hint: "Pass a trusted local JavaScript module exporting a default ToolClient program.",
|
|
4525
|
+
detail: {}
|
|
4526
|
+
}
|
|
4527
|
+
};
|
|
4528
|
+
}
|
|
4529
|
+
const root = options.root ?? process.cwd();
|
|
4530
|
+
try {
|
|
4531
|
+
const module = await import(pathToFileURL(resolve(root, options.program)).href);
|
|
4532
|
+
const program = module.default ?? module.run;
|
|
4533
|
+
if (typeof program !== "function") {
|
|
4534
|
+
throw new TypeError("operation program must export a default function or named run function");
|
|
4535
|
+
}
|
|
4536
|
+
const client = await createToolClient({ projectRoot: root });
|
|
4537
|
+
const value = await program(client);
|
|
4538
|
+
if (!isSerializableValue(value)) {
|
|
4539
|
+
throw new TypeError("tool program returned a non-serializable live value");
|
|
4540
|
+
}
|
|
4541
|
+
return { ok: true, value };
|
|
4542
|
+
} catch (cause) {
|
|
4543
|
+
return {
|
|
4544
|
+
ok: false,
|
|
4545
|
+
error: {
|
|
4546
|
+
code: "tool-program-failed",
|
|
4547
|
+
expected: "the trusted local operation program to complete with serializable results",
|
|
4548
|
+
hint: "Repair the program module or inspect its selected operation terminal.",
|
|
4549
|
+
detail: { reason: cause instanceof Error ? cause.message : String(cause) }
|
|
4550
|
+
}
|
|
4551
|
+
};
|
|
4552
|
+
}
|
|
4553
|
+
}
|
|
4554
|
+
|
|
4555
|
+
// src/cli.ts
|
|
4556
|
+
var args = process.argv.slice(2);
|
|
4557
|
+
var primary = args.shift();
|
|
4558
|
+
var nested = primary === "asset" || primary === "shader" || primary === "plugin" || primary === "skill" || primary === "sdk" ? args.shift() : void 0;
|
|
4559
|
+
var command = primary === "asset" || primary === "shader" || primary === "plugin" || primary === "skill" || primary === "sdk" ? `${primary}.${nested ?? ""}` : primary;
|
|
4560
|
+
var json = args.includes("--json");
|
|
4561
|
+
var dryRun = args.includes("--dry-run");
|
|
4562
|
+
var noInstall = args.includes("--no-install");
|
|
4563
|
+
function option(name) {
|
|
4564
|
+
const index = args.indexOf(name);
|
|
4565
|
+
return index < 0 ? void 0 : args[index + 1];
|
|
4566
|
+
}
|
|
4567
|
+
var optionValueIndexes = new Set(
|
|
4568
|
+
[
|
|
4569
|
+
"--artifact",
|
|
4570
|
+
"--base",
|
|
4571
|
+
"--digest",
|
|
4572
|
+
"--id",
|
|
4573
|
+
"--realm",
|
|
4574
|
+
"--dependency",
|
|
4575
|
+
"--out-dir",
|
|
4576
|
+
"--input",
|
|
4577
|
+
"--output",
|
|
4578
|
+
"--root",
|
|
4579
|
+
"--template",
|
|
4580
|
+
"--version",
|
|
4581
|
+
"--work-index"
|
|
4582
|
+
].flatMap((name) => {
|
|
4583
|
+
const index = args.indexOf(name);
|
|
4584
|
+
return index < 0 ? [] : [index + 1];
|
|
4585
|
+
})
|
|
4586
|
+
);
|
|
4587
|
+
var positionals = args.filter(
|
|
4588
|
+
(value, index) => !value.startsWith("--") && !optionValueIndexes.has(index)
|
|
4589
|
+
);
|
|
4590
|
+
var scopedRoot = option("--root") ?? process.cwd();
|
|
4591
|
+
var projectRoot = positionals[0] ?? scopedRoot;
|
|
4592
|
+
var commands = [
|
|
4593
|
+
"new",
|
|
4594
|
+
"init",
|
|
4595
|
+
"doctor",
|
|
4596
|
+
"test",
|
|
4597
|
+
"dev",
|
|
4598
|
+
"build",
|
|
4599
|
+
"package",
|
|
4600
|
+
"serve",
|
|
4601
|
+
"preview",
|
|
4602
|
+
"run",
|
|
4603
|
+
"plugin.install",
|
|
4604
|
+
"plugin.uninstall",
|
|
4605
|
+
"skill.install",
|
|
4606
|
+
"skill.verify",
|
|
4607
|
+
"sdk.install",
|
|
4608
|
+
"asset.add",
|
|
4609
|
+
"asset.verify",
|
|
4610
|
+
"asset.inspect",
|
|
4611
|
+
"asset.list",
|
|
4612
|
+
"shader.check",
|
|
4613
|
+
"list",
|
|
4614
|
+
"describe",
|
|
4615
|
+
"exec"
|
|
4616
|
+
];
|
|
4617
|
+
async function run(value) {
|
|
4618
|
+
switch (value) {
|
|
4619
|
+
case "new": {
|
|
4620
|
+
const template = option("--template");
|
|
4621
|
+
return newCommand({
|
|
4622
|
+
root: projectRoot,
|
|
4623
|
+
dryRun,
|
|
4624
|
+
...template === void 0 ? {} : { template }
|
|
4625
|
+
});
|
|
4626
|
+
}
|
|
4627
|
+
case "init":
|
|
4628
|
+
return initCommand({ root: projectRoot, dryRun, install: !noInstall });
|
|
4629
|
+
case "doctor":
|
|
4630
|
+
return doctorCommand({ root: projectRoot});
|
|
4631
|
+
case "test":
|
|
4632
|
+
return testCommand({ root: projectRoot});
|
|
4633
|
+
case "dev":
|
|
4634
|
+
return devCommand({ root: projectRoot, json });
|
|
4635
|
+
case "build": {
|
|
4636
|
+
const base = option("--base");
|
|
4637
|
+
const outDir = option("--out-dir");
|
|
4638
|
+
return buildCommand({
|
|
4639
|
+
root: projectRoot,
|
|
4640
|
+
...base === void 0 ? {} : { base },
|
|
4641
|
+
...outDir === void 0 ? {} : { outDir }
|
|
4642
|
+
});
|
|
4643
|
+
}
|
|
4644
|
+
case "package": {
|
|
4645
|
+
const output = option("--output");
|
|
4646
|
+
return packageCommand({
|
|
4647
|
+
root: projectRoot,
|
|
4648
|
+
json,
|
|
4649
|
+
...output === void 0 ? {} : { output }
|
|
4650
|
+
});
|
|
4651
|
+
}
|
|
4652
|
+
case "preview":
|
|
4653
|
+
return previewCommand({ root: projectRoot, json });
|
|
4654
|
+
case "serve":
|
|
4655
|
+
return devCommand({ root: projectRoot, json });
|
|
4656
|
+
case "plugin.install": {
|
|
4657
|
+
const module = positionals[0];
|
|
4658
|
+
const id = option("--id");
|
|
4659
|
+
const realm = option("--realm");
|
|
4660
|
+
const dependency = option("--dependency");
|
|
4661
|
+
if (module === void 0 || id === void 0 || realm !== void 0 && realm !== "host" && realm !== "engine" && realm !== "build") {
|
|
4662
|
+
return {
|
|
4663
|
+
ok: false,
|
|
4664
|
+
error: {
|
|
4665
|
+
code: "cli-parse-error",
|
|
4666
|
+
expected: "forgeax plugin install <module> --id <id> [--realm host|engine|build]",
|
|
4667
|
+
hint: "Pass a stable Entry id and an optional physical realm.",
|
|
4668
|
+
detail: {}
|
|
4669
|
+
}
|
|
4670
|
+
};
|
|
4671
|
+
}
|
|
4672
|
+
return pluginInstallCommand({
|
|
4673
|
+
root: option("--root") ?? process.cwd(),
|
|
4674
|
+
module,
|
|
4675
|
+
id,
|
|
4676
|
+
dryRun,
|
|
4677
|
+
...realm === void 0 ? {} : { realm },
|
|
4678
|
+
...dependency === void 0 ? {} : { dependency }
|
|
4679
|
+
});
|
|
4680
|
+
}
|
|
4681
|
+
case "plugin.uninstall": {
|
|
4682
|
+
const id = positionals[0];
|
|
4683
|
+
const dependency = option("--dependency");
|
|
4684
|
+
if (id === void 0) {
|
|
4685
|
+
return {
|
|
4686
|
+
ok: false,
|
|
4687
|
+
error: {
|
|
4688
|
+
code: "cli-parse-error",
|
|
4689
|
+
expected: "forgeax plugin uninstall <entry-id>",
|
|
4690
|
+
hint: "Pass the stable Entry id stored in forge.json#plugins.",
|
|
4691
|
+
detail: {}
|
|
4692
|
+
}
|
|
4693
|
+
};
|
|
4694
|
+
}
|
|
4695
|
+
return pluginUninstallCommand({
|
|
4696
|
+
root: option("--root") ?? process.cwd(),
|
|
4697
|
+
id,
|
|
4698
|
+
dryRun,
|
|
4699
|
+
...dependency === void 0 ? {} : { dependency }
|
|
4700
|
+
});
|
|
4701
|
+
}
|
|
4702
|
+
case "skill.install":
|
|
4703
|
+
return skillInstallCommand({ root: scopedRoot});
|
|
4704
|
+
case "skill.verify":
|
|
4705
|
+
return skillVerifyCommand({ root: scopedRoot});
|
|
4706
|
+
case "sdk.install": {
|
|
4707
|
+
const root = positionals[0];
|
|
4708
|
+
if (root === void 0) {
|
|
4709
|
+
return {
|
|
4710
|
+
ok: false,
|
|
4711
|
+
error: {
|
|
4712
|
+
code: "cli-parse-error",
|
|
4713
|
+
expected: "forgeax sdk install <directory> [--version VERSION]",
|
|
4714
|
+
hint: "Pass an empty destination outside existing SDK and game directories.",
|
|
4715
|
+
detail: {}
|
|
4716
|
+
}
|
|
4717
|
+
};
|
|
4718
|
+
}
|
|
4719
|
+
const version = option("--version");
|
|
4720
|
+
return sdkInstallCommand({ root, ...version === void 0 ? {} : { version } });
|
|
4721
|
+
}
|
|
4722
|
+
case "asset.add": {
|
|
4723
|
+
const path = positionals[0];
|
|
4724
|
+
if (path === void 0) {
|
|
4725
|
+
return {
|
|
4726
|
+
ok: false,
|
|
4727
|
+
error: {
|
|
4728
|
+
code: "cli-parse-error",
|
|
4729
|
+
expected: "forgeax asset add <path>",
|
|
4730
|
+
hint: "Pass one source file or directory.",
|
|
4731
|
+
detail: {}
|
|
4732
|
+
}
|
|
4733
|
+
};
|
|
4734
|
+
}
|
|
4735
|
+
return assetAddCommand({ root: process.cwd(), path, dryRun, json });
|
|
4736
|
+
}
|
|
4737
|
+
case "asset.verify":
|
|
4738
|
+
return assetVerifyCommand({ root: process.cwd()});
|
|
4739
|
+
case "asset.inspect": {
|
|
4740
|
+
const subject = positionals[0];
|
|
4741
|
+
if (subject === void 0) {
|
|
4742
|
+
return {
|
|
4743
|
+
ok: false,
|
|
4744
|
+
error: {
|
|
4745
|
+
code: "cli-parse-error",
|
|
4746
|
+
expected: "forgeax asset inspect <guid-or-name>",
|
|
4747
|
+
hint: "Pass a stable asset GUID or an unambiguous name.",
|
|
4748
|
+
detail: {}
|
|
4749
|
+
}
|
|
4750
|
+
};
|
|
4751
|
+
}
|
|
4752
|
+
return assetInspectCommand({ root: process.cwd(), subject});
|
|
4753
|
+
}
|
|
4754
|
+
case "asset.list":
|
|
4755
|
+
return assetListCommand({ root: process.cwd()});
|
|
4756
|
+
case "shader.check":
|
|
4757
|
+
return shaderCheckCommand({
|
|
4758
|
+
root: process.cwd(),
|
|
4759
|
+
...positionals[0] === void 0 ? {} : { path: positionals[0] }
|
|
4760
|
+
});
|
|
4761
|
+
case "list":
|
|
4762
|
+
return listCommand(scopedRoot);
|
|
4763
|
+
case "describe":
|
|
4764
|
+
return describeCommand({
|
|
4765
|
+
root: scopedRoot,
|
|
4766
|
+
...positionals[0] === void 0 ? {} : { id: positionals[0] }
|
|
4767
|
+
});
|
|
4768
|
+
case "exec":
|
|
4769
|
+
return execCommand({
|
|
4770
|
+
root: scopedRoot,
|
|
4771
|
+
...positionals[0] === void 0 ? {} : { program: positionals[0] }
|
|
4772
|
+
});
|
|
4773
|
+
case "run": {
|
|
4774
|
+
const operation = positionals[0];
|
|
4775
|
+
if (operation === void 0 || args.includes("--help")) {
|
|
4776
|
+
return { ok: true, value: { help: renderRhiDebugHelp() } };
|
|
4777
|
+
}
|
|
4778
|
+
if (!operation.startsWith("rhi.")) {
|
|
4779
|
+
const input = option("--input");
|
|
4780
|
+
return runCommand({
|
|
4781
|
+
root: scopedRoot,
|
|
4782
|
+
id: operation,
|
|
4783
|
+
...input === void 0 ? {} : { input }
|
|
4784
|
+
});
|
|
4785
|
+
}
|
|
4786
|
+
if (operation !== "rhi.capture" && operation !== "rhi.summary" && operation !== "rhi.inspect") {
|
|
4787
|
+
return {
|
|
4788
|
+
ok: false,
|
|
4789
|
+
error: {
|
|
4790
|
+
code: "cli-parse-error",
|
|
4791
|
+
expected: "rhi.capture, rhi.summary, or rhi.inspect",
|
|
4792
|
+
hint: "Run forgeax run --help to discover the three RHI debug operations.",
|
|
4793
|
+
detail: { operation }
|
|
4794
|
+
}
|
|
4795
|
+
};
|
|
4796
|
+
}
|
|
4797
|
+
const context = createCliRhiDebugOperationContext();
|
|
4798
|
+
if (operation === "rhi.capture") return runRhiDebugCommand(operation, {}, context);
|
|
4799
|
+
const path = option("--artifact");
|
|
4800
|
+
const digest = option("--digest");
|
|
4801
|
+
if (path === void 0 || digest === void 0) {
|
|
4802
|
+
return {
|
|
4803
|
+
ok: false,
|
|
4804
|
+
error: {
|
|
4805
|
+
code: "cli-parse-error",
|
|
4806
|
+
expected: "--artifact PATH and --digest DIGEST",
|
|
4807
|
+
hint: "Pass the same ArtifactRef path and digest returned by capture.",
|
|
4808
|
+
detail: {}
|
|
4809
|
+
}
|
|
4810
|
+
};
|
|
4811
|
+
}
|
|
4812
|
+
const artifact = { kind: "rhi-tape", digest, source: "forgeax", path };
|
|
4813
|
+
if (operation === "rhi.summary") {
|
|
4814
|
+
return runRhiDebugCommand(operation, { artifact }, context);
|
|
4815
|
+
}
|
|
4816
|
+
const workIndex = Number(option("--work-index"));
|
|
4817
|
+
if (!Number.isInteger(workIndex) || workIndex < 0) {
|
|
4818
|
+
return {
|
|
4819
|
+
ok: false,
|
|
4820
|
+
error: {
|
|
4821
|
+
code: "cli-parse-error",
|
|
4822
|
+
expected: "--work-index to be a non-negative integer",
|
|
4823
|
+
hint: "Choose workIndex from rhi.summary output.",
|
|
4824
|
+
detail: { workIndex: option("--work-index") ?? null }
|
|
4825
|
+
}
|
|
4826
|
+
};
|
|
4827
|
+
}
|
|
4828
|
+
return runRhiDebugCommand(operation, { artifact, workIndex }, context);
|
|
4829
|
+
}
|
|
4830
|
+
}
|
|
4831
|
+
}
|
|
4832
|
+
if (command === void 0 || !commands.includes(command)) {
|
|
4833
|
+
process.stderr.write(
|
|
4834
|
+
"Usage: forgeax new [directory] [--template empty|game-default]\n forgeax <init|doctor|test|dev|build|package|serve|preview> [directory]\n forgeax package [directory] [--output release/game-web.zip]\n forgeax run <rhi.capture|rhi.summary|rhi.inspect> [options]\n forgeax run <operation-id> --input <request.json>\n forgeax asset <add|verify|inspect|list> [subject]\n forgeax shader check [path]\n forgeax plugin <install|uninstall> <module-or-id> [options]\n forgeax skill <install|verify> [--root directory]\n forgeax sdk install <directory> [--version VERSION]\n"
|
|
4835
|
+
);
|
|
4836
|
+
process.exitCode = 2;
|
|
4837
|
+
} else {
|
|
4838
|
+
const stdoutWrite = process.stdout.write;
|
|
4839
|
+
if (json) {
|
|
4840
|
+
process.stdout.write = ((...writeArgs) => Reflect.apply(
|
|
4841
|
+
process.stderr.write,
|
|
4842
|
+
process.stderr,
|
|
4843
|
+
writeArgs
|
|
4844
|
+
));
|
|
4845
|
+
}
|
|
4846
|
+
let result;
|
|
4847
|
+
try {
|
|
4848
|
+
result = await run(command);
|
|
4849
|
+
} finally {
|
|
4850
|
+
process.stdout.write = stdoutWrite;
|
|
4851
|
+
}
|
|
4852
|
+
const envelope = result.ok ? { schemaVersion: "1.0.0", command, ok: true, value: result.value } : { schemaVersion: "1.0.0", command, ok: false, error: result.error };
|
|
4853
|
+
if (json) stdoutWrite.call(process.stdout, `${JSON.stringify(envelope)}
|
|
4854
|
+
`);
|
|
4855
|
+
else if (result.ok) process.stdout.write(`[forgeax] ${command} ready
|
|
4856
|
+
`);
|
|
4857
|
+
else process.stderr.write(`[forgeax] ${result.error.code}: ${result.error.hint}
|
|
4858
|
+
`);
|
|
4859
|
+
if (!result.ok) process.exitCode = result.error.code === "cli-parse-error" ? 2 : 1;
|
|
4860
|
+
}
|
|
4861
|
+
//# sourceMappingURL=cli.mjs.map
|
|
4862
|
+
//# sourceMappingURL=cli.mjs.map
|