@forgeax/game 0.3.6 → 0.3.8
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/README.md +16 -130
- package/assets/extensions/asset3d/cli.mjs +2090 -0
- package/assets/extensions/asset3d/extension.json +7 -0
- package/assets/extensions/asset3d/skills/art-3d-asset-library/SKILL.md +28 -0
- package/assets/licenses/fflate.txt +21 -0
- package/assets/skills/forgeax-game/SKILL.md +4 -7
- package/dist/main.js +582 -2460
- package/docs/asset3d.md +43 -193
- package/docs/plugin-integration-standard.md +196 -0
- package/package.json +3 -9
- package/assets/asset3d/adapter/SKILL.md +0 -56
- package/assets/asset3d/provider/asset3d-search-provider-a274238712c1dced99e4aa3148a0d15904ce2a90-darwin-arm64.tar.gz +0 -0
- package/assets/asset3d/provider/asset3d-search-provider-a274238712c1dced99e4aa3148a0d15904ce2a90-linux-x64.tar.gz +0 -0
- package/assets/asset3d/schemas/receipt-1.0.0.json +0 -29
- package/assets/asset3d/schemas/result-1.0.0.json +0 -361
- package/assets/asset3d/vibegame-art-3d-asset-library-2.0.0.tgz +0 -0
- package/assets/asset3d/vibegame-art-3d-asset-library-2.0.0.tgz.sha256 +0 -1
- package/docs/release-0.3.0.md +0 -100
- package/docs/release-0.3.1.md +0 -107
- package/docs/release-0.3.2.md +0 -90
- package/docs/release-0.3.3.md +0 -73
- package/docs/release-0.3.4.md +0 -66
- package/docs/release-0.3.5.md +0 -32
package/dist/main.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/main.ts
|
|
4
|
-
import { resolve as
|
|
4
|
+
import { resolve as resolve14 } from "node:path";
|
|
5
5
|
|
|
6
6
|
// src/mcp/protocol.ts
|
|
7
7
|
var MCP_PROTOCOL_VERSION = "2024-11-05";
|
|
@@ -416,11 +416,10 @@ reasoning and game-code edits; the plugin owns the exact Engine Preview adapter.
|
|
|
416
416
|
public https URL, or a local file path when COS is configured (it is uploaded and
|
|
417
417
|
passed as a short-lived presigned URL). They need \`FORGEAX_LITELLM_API_KEY\` (and
|
|
418
418
|
\`FORGEAX_COS_*\` for local-file image-to-3D) in the environment.
|
|
419
|
-
- Reusing
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
for an explicitly bespoke asset or a user-approved fallback after an empty search.
|
|
419
|
+
- Reusing a library 3D asset: read the installed \`art-3d-asset-library\` Skill
|
|
420
|
+
and use its pinned CLI to search candidates and import the selected ID.
|
|
421
|
+
If not enabled, report the missing setup. Do not relabel procedural geometry
|
|
422
|
+
or generated models as library results.
|
|
424
423
|
- Creating a game, switching the active game, installing or upgrading the plugin:
|
|
425
424
|
these are one-time operations and are CLI subcommands, not MCP tools. Run
|
|
426
425
|
\`npx -y @forgeax/game <init|use|doctor|devkit|upgrade>\`.
|
|
@@ -514,6 +513,7 @@ function sameFile(left, right) {
|
|
|
514
513
|
}
|
|
515
514
|
function copySkill(source, destination) {
|
|
516
515
|
const destinationIsSymlink = existsSync(destination) && lstatSync(destination).isSymbolicLink();
|
|
516
|
+
const linksToSource = destinationIsSymlink && realpathSync2(source) === realpathSync2(destination);
|
|
517
517
|
if (!destinationIsSymlink && existsSync(destination) && realpathSync2(source) === realpathSync2(destination))
|
|
518
518
|
return false;
|
|
519
519
|
const files = filesUnder(source);
|
|
@@ -521,9 +521,11 @@ function copySkill(source, destination) {
|
|
|
521
521
|
if (!changed)
|
|
522
522
|
return false;
|
|
523
523
|
if (existsSync(destination)) {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
524
|
+
if (!linksToSource) {
|
|
525
|
+
const backup = `${destination}.bak.latest`;
|
|
526
|
+
rmSync(backup, { recursive: true, force: true });
|
|
527
|
+
cpSync(destination, backup, { recursive: true });
|
|
528
|
+
}
|
|
527
529
|
rmSync(destination, { recursive: true, force: true });
|
|
528
530
|
}
|
|
529
531
|
for (const path of files) {
|
|
@@ -2286,7 +2288,6 @@ import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as rea
|
|
|
2286
2288
|
import { basename as basename2, extname as extname2, join as join7, relative as relative5 } from "node:path";
|
|
2287
2289
|
|
|
2288
2290
|
// src/gen/config.ts
|
|
2289
|
-
var DEFAULT_LITELLM_BASE_URL = "http://21.214.33.175:4000";
|
|
2290
2291
|
var DEFAULT_MODELS = {
|
|
2291
2292
|
textToImage: "gemini-3-pro-image",
|
|
2292
2293
|
textTo3d: "tripo-3d-text",
|
|
@@ -2297,11 +2298,15 @@ function env(name) {
|
|
|
2297
2298
|
return value ? value : undefined;
|
|
2298
2299
|
}
|
|
2299
2300
|
function resolveLiteLlmConfig() {
|
|
2300
|
-
const baseUrl = (env("FORGEAX_LITELLM_BASE_URL") ?? DEFAULT_LITELLM_BASE_URL).replace(/\/+$/, "");
|
|
2301
2301
|
const apiKey = env("FORGEAX_LITELLM_API_KEY");
|
|
2302
2302
|
if (!apiKey) {
|
|
2303
2303
|
throw new Error("FORGEAX_LITELLM_API_KEY is not set. Export the LiteLLM key so the asset tools can reach the gateway, e.g. `export FORGEAX_LITELLM_API_KEY=sk-...`.");
|
|
2304
2304
|
}
|
|
2305
|
+
const configuredBaseUrl = env("FORGEAX_LITELLM_BASE_URL");
|
|
2306
|
+
if (!configuredBaseUrl) {
|
|
2307
|
+
throw new Error("FORGEAX_LITELLM_BASE_URL is not set. Set the LiteLLM gateway URL before using the asset generation tools.");
|
|
2308
|
+
}
|
|
2309
|
+
const baseUrl = configuredBaseUrl.replace(/\/+$/, "");
|
|
2305
2310
|
return {
|
|
2306
2311
|
baseUrl,
|
|
2307
2312
|
apiKey,
|
|
@@ -2680,13 +2685,13 @@ async function generate3dTool(args, cwd) {
|
|
|
2680
2685
|
const image = typeof args.image === "string" ? args.image.trim() : "";
|
|
2681
2686
|
if (!prompt && !image)
|
|
2682
2687
|
throw new Error("Provide `prompt` (text-to-3D) or `image` (image-to-3D).");
|
|
2683
|
-
const cfg = resolveLiteLlmConfig();
|
|
2684
2688
|
const targetDir = typeof args.target_dir === "string" ? args.target_dir : cwd;
|
|
2685
2689
|
const { dir, root, slug } = assetsDirFor(targetDir, typeof args.game === "string" ? args.game : undefined);
|
|
2690
|
+
const imageUrl = image ? await resolveImageUrlFor3d(image, slug) : undefined;
|
|
2691
|
+
const cfg = resolveLiteLlmConfig();
|
|
2686
2692
|
let result;
|
|
2687
2693
|
let mode;
|
|
2688
|
-
if (
|
|
2689
|
-
const imageUrl = await resolveImageUrlFor3d(image, slug);
|
|
2694
|
+
if (imageUrl) {
|
|
2690
2695
|
const model = typeof args.model === "string" && args.model.trim() ? args.model.trim() : cfg.models.imageTo3d;
|
|
2691
2696
|
result = await generate3dFromImageUrl(cfg, { model, imageUrl, prompt: prompt || undefined, onProgress: logProgress("image-to-3D") });
|
|
2692
2697
|
mode = "image-to-3D";
|
|
@@ -2945,6 +2950,68 @@ function gameFileTools() {
|
|
|
2945
2950
|
}
|
|
2946
2951
|
];
|
|
2947
2952
|
}
|
|
2953
|
+
// package.json
|
|
2954
|
+
var package_default = {
|
|
2955
|
+
packageManager: "bun@1.4.0",
|
|
2956
|
+
name: "@forgeax/game",
|
|
2957
|
+
version: "0.3.8",
|
|
2958
|
+
private: false,
|
|
2959
|
+
type: "module",
|
|
2960
|
+
description: "@forgeax/game — an MCP/CLI connector for exact released ForgeaX Engine games and Engine-owned Preview.",
|
|
2961
|
+
main: "./dist/main.js",
|
|
2962
|
+
bin: {
|
|
2963
|
+
"forgeax-game": "dist/main.js",
|
|
2964
|
+
game: "dist/main.js"
|
|
2965
|
+
},
|
|
2966
|
+
engines: {
|
|
2967
|
+
node: ">=22.13.0"
|
|
2968
|
+
},
|
|
2969
|
+
publishConfig: {
|
|
2970
|
+
access: "public",
|
|
2971
|
+
registry: "https://registry.npmjs.org/"
|
|
2972
|
+
},
|
|
2973
|
+
files: [
|
|
2974
|
+
"dist",
|
|
2975
|
+
"assets",
|
|
2976
|
+
"docs/asset3d.md",
|
|
2977
|
+
"docs/plugin-integration-standard.md",
|
|
2978
|
+
"README.md"
|
|
2979
|
+
],
|
|
2980
|
+
scripts: {
|
|
2981
|
+
build: "bun build.mjs",
|
|
2982
|
+
"release:check": "bun scripts/check-package-artifact.ts",
|
|
2983
|
+
acceptance: "bun scripts/accept-packed-consumer.ts",
|
|
2984
|
+
"release:publish": "bun scripts/publish-package.ts",
|
|
2985
|
+
prepack: "bun build.mjs",
|
|
2986
|
+
typecheck: "tsc --noEmit",
|
|
2987
|
+
test: "bun test test/*.test.ts",
|
|
2988
|
+
start: "bun src/main.ts",
|
|
2989
|
+
"mcp:inspect": "npx @modelcontextprotocol/inspector node dist/main.js"
|
|
2990
|
+
},
|
|
2991
|
+
keywords: [
|
|
2992
|
+
"forgeax",
|
|
2993
|
+
"mcp",
|
|
2994
|
+
"model-context-protocol",
|
|
2995
|
+
"game-development",
|
|
2996
|
+
"codex",
|
|
2997
|
+
"claude-code"
|
|
2998
|
+
],
|
|
2999
|
+
license: "MIT",
|
|
3000
|
+
dependencies: {
|
|
3001
|
+
"@forgeax/engine-sdk": "0.1.26",
|
|
3002
|
+
pnpm: "11.7.0"
|
|
3003
|
+
},
|
|
3004
|
+
devDependencies: {
|
|
3005
|
+
fflate: "0.8.2",
|
|
3006
|
+
"@types/bun": "^1.2.0",
|
|
3007
|
+
typescript: "^5.9.2"
|
|
3008
|
+
},
|
|
3009
|
+
directories: {
|
|
3010
|
+
doc: "docs",
|
|
3011
|
+
test: "test"
|
|
3012
|
+
},
|
|
3013
|
+
author: ""
|
|
3014
|
+
};
|
|
2948
3015
|
|
|
2949
3016
|
// src/install/release-manifest.ts
|
|
2950
3017
|
var RELEASE_IDENTITY_SCHEMA = "forgeax.game.release-identity/1";
|
|
@@ -2953,7 +3020,7 @@ var RELEASE_IDENTITY_MIME = "application/vnd.forgeax.game-release-identity+json"
|
|
|
2953
3020
|
var RELEASE_IDENTITY = Object.freeze({
|
|
2954
3021
|
schema: RELEASE_IDENTITY_SCHEMA,
|
|
2955
3022
|
gamePackage: "@forgeax/game",
|
|
2956
|
-
gameVersion:
|
|
3023
|
+
gameVersion: package_default.version,
|
|
2957
3024
|
gameBin: "forgeax-game",
|
|
2958
3025
|
engineSdkPackage: ENGINE_SDK_PACKAGE,
|
|
2959
3026
|
engineSdkVersion: ENGINE_VERSION,
|
|
@@ -3233,9 +3300,8 @@ async function startHttpMcpServer(spec, options) {
|
|
|
3233
3300
|
}
|
|
3234
3301
|
|
|
3235
3302
|
// src/cli/dispatch.ts
|
|
3236
|
-
import { existsSync as
|
|
3237
|
-
import {
|
|
3238
|
-
import { join as join12, resolve as resolve16 } from "node:path";
|
|
3303
|
+
import { existsSync as existsSync9, readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "node:fs";
|
|
3304
|
+
import { join as join11 } from "node:path";
|
|
3239
3305
|
|
|
3240
3306
|
// src/install/clients.ts
|
|
3241
3307
|
import { homedir as homedir2 } from "node:os";
|
|
@@ -3356,30 +3422,19 @@ function findClient(id) {
|
|
|
3356
3422
|
return CLIENTS.find((client) => client.id === id || client.aliases?.includes(id));
|
|
3357
3423
|
}
|
|
3358
3424
|
var SERVER_KEY = "forgeax";
|
|
3359
|
-
var ASSET3D_SERVER_KEY = "asset3d-search";
|
|
3360
3425
|
function launchSpec(mode) {
|
|
3361
3426
|
if (mode === "local") {
|
|
3362
3427
|
return { command: process.execPath, args: [resolve9(process.argv[1] ?? ""), "mcp"] };
|
|
3363
3428
|
}
|
|
3364
3429
|
return {
|
|
3365
3430
|
command: "npx",
|
|
3366
|
-
args: ["-y", "-p",
|
|
3367
|
-
};
|
|
3368
|
-
}
|
|
3369
|
-
function asset3dLaunchSpec(mode) {
|
|
3370
|
-
const launch = launchSpec(mode);
|
|
3371
|
-
if (launch.args.at(-1) !== "mcp") {
|
|
3372
|
-
throw new Error("asset3d_game_plugin_launch_invalid: expected MCP launch suffix");
|
|
3373
|
-
}
|
|
3374
|
-
return {
|
|
3375
|
-
command: launch.command,
|
|
3376
|
-
args: [...launch.args.slice(0, -1), "asset3d", "mcp"]
|
|
3431
|
+
args: ["-y", "-p", `@forgeax/game@${RELEASE_IDENTITY.gameVersion}`, "forgeax-game", "mcp"]
|
|
3377
3432
|
};
|
|
3378
3433
|
}
|
|
3379
3434
|
|
|
3380
3435
|
// src/install/write-config.ts
|
|
3381
3436
|
import { copyFileSync as copyFileSync2, existsSync as existsSync7, mkdirSync as mkdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "node:fs";
|
|
3382
|
-
import { dirname as dirname8 } from "node:path";
|
|
3437
|
+
import { basename as basename3, dirname as dirname8, isAbsolute as isAbsolute3, resolve as resolve10 } from "node:path";
|
|
3383
3438
|
|
|
3384
3439
|
// src/install/toml-section.ts
|
|
3385
3440
|
var HEADER_RE = /^[ \t]*\[([^[\]\r\n]+)\][ \t]*(?:#[^\r\n]*)?\r?$/gm;
|
|
@@ -4030,7 +4085,7 @@ function applyConfig(spec, projectRoot, launch, serverKey = SERVER_KEY) {
|
|
|
4030
4085
|
writeFileSync7(path, merged.content);
|
|
4031
4086
|
return { path, changed: true, ...backup ? { backup } : {} };
|
|
4032
4087
|
}
|
|
4033
|
-
function removeConfig(spec, projectRoot, serverKey = SERVER_KEY) {
|
|
4088
|
+
function removeConfig(spec, projectRoot, serverKey = SERVER_KEY, backupSuffix = ".bak.latest") {
|
|
4034
4089
|
const path = spec.path(projectRoot);
|
|
4035
4090
|
if (!existsSync7(path))
|
|
4036
4091
|
return { path, changed: false };
|
|
@@ -4060,11 +4115,56 @@ function removeConfig(spec, projectRoot, serverKey = SERVER_KEY) {
|
|
|
4060
4115
|
}
|
|
4061
4116
|
if (content === existing)
|
|
4062
4117
|
return { path, changed: false };
|
|
4063
|
-
const backup = `${path}
|
|
4118
|
+
const backup = `${path}${backupSuffix}`;
|
|
4064
4119
|
copyFileSync2(path, backup);
|
|
4065
4120
|
writeFileSync7(path, content);
|
|
4066
4121
|
return { path, changed: true, backup };
|
|
4067
4122
|
}
|
|
4123
|
+
function retireAsset3dConfig(spec, projectRoot) {
|
|
4124
|
+
const key = "asset3d-search";
|
|
4125
|
+
const path = spec.path(projectRoot);
|
|
4126
|
+
if (!existsSync7(path))
|
|
4127
|
+
return "absent";
|
|
4128
|
+
try {
|
|
4129
|
+
const text = readFileSync11(path, "utf8");
|
|
4130
|
+
let entry;
|
|
4131
|
+
if (spec.format === "toml") {
|
|
4132
|
+
const table = readTomlTable(text, `mcp_servers.${key}`);
|
|
4133
|
+
if (table === undefined) {
|
|
4134
|
+
return hasCompetingTomlDefinition(text, `mcp_servers.${key}`) ? "preserved" : "absent";
|
|
4135
|
+
}
|
|
4136
|
+
const command2 = table.match(/^command\s*=\s*(".*")\s*$/m)?.[1];
|
|
4137
|
+
const args2 = table.match(/^args\s*=\s*(\[.*\])\s*$/m)?.[1];
|
|
4138
|
+
if (!command2 || !args2)
|
|
4139
|
+
return "preserved";
|
|
4140
|
+
entry = { command: JSON.parse(command2), args: JSON.parse(args2) };
|
|
4141
|
+
const expected = readTomlTable(mergeTomlConfig(undefined, entry, key).content, `mcp_servers.${key}`);
|
|
4142
|
+
if (table.trim() !== expected?.trim() || /^\s*\[.*asset3d-search.*\.\s*[\w"']/m.test(text))
|
|
4143
|
+
return "preserved";
|
|
4144
|
+
} else {
|
|
4145
|
+
entry = jsonServerEntry(JSON.parse(text), spec, key);
|
|
4146
|
+
if (entry === undefined)
|
|
4147
|
+
return "absent";
|
|
4148
|
+
}
|
|
4149
|
+
if (!entry || typeof entry !== "object")
|
|
4150
|
+
return "preserved";
|
|
4151
|
+
const command = spec.commandShape === "argv" && Array.isArray(entry.command) ? entry.command[0] : entry.command;
|
|
4152
|
+
const args = spec.commandShape === "argv" && Array.isArray(entry.command) ? entry.command.slice(1) : entry.args;
|
|
4153
|
+
if (typeof command !== "string" || !Array.isArray(args) || !args.every((x) => typeof x === "string"))
|
|
4154
|
+
return "preserved";
|
|
4155
|
+
const published = command === "npx" && args.length === 6 && args[0] === "-y" && args[1] === "-p" && /^@forgeax\/game@\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(args[2]) && args[3] === "forgeax-game" && args[4] === "asset3d" && args[5] === "mcp";
|
|
4156
|
+
let local = false;
|
|
4157
|
+
if (isAbsolute3(command) && /^(node|node.exe)$/.test(basename3(command)) && args.length === 3 && isAbsolute3(args[0]) && args[1] === "asset3d" && args[2] === "mcp" && basename3(args[0]) === "main.js" && basename3(dirname8(args[0])) === "dist") {
|
|
4158
|
+
const pkg = JSON.parse(readFileSync11(resolve10(dirname8(args[0]), "..", "package.json"), "utf8"));
|
|
4159
|
+
local = pkg.name === "@forgeax/game";
|
|
4160
|
+
}
|
|
4161
|
+
if (!(published || local) || inspectConfig(spec, projectRoot, { command, args }, key).state !== "current")
|
|
4162
|
+
return "preserved";
|
|
4163
|
+
return removeConfig(spec, projectRoot, key, ".asset3d-retired.bak").changed ? "removed" : "absent";
|
|
4164
|
+
} catch {
|
|
4165
|
+
return "preserved";
|
|
4166
|
+
}
|
|
4167
|
+
}
|
|
4068
4168
|
|
|
4069
4169
|
// src/install/verify.ts
|
|
4070
4170
|
import { spawn as spawn2 } from "node:child_process";
|
|
@@ -4075,8 +4175,8 @@ function commandText(launch) {
|
|
|
4075
4175
|
return [launch.command, ...launch.args].map((part) => JSON.stringify(part)).join(" ");
|
|
4076
4176
|
}
|
|
4077
4177
|
function rpcRequest(child, pending, id, method, params = {}) {
|
|
4078
|
-
return new Promise((
|
|
4079
|
-
pending.set(id,
|
|
4178
|
+
return new Promise((resolve11, reject) => {
|
|
4179
|
+
pending.set(id, resolve11);
|
|
4080
4180
|
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}
|
|
4081
4181
|
`, (error) => {
|
|
4082
4182
|
if (!error)
|
|
@@ -4141,11 +4241,11 @@ async function verifyLaunchInternal(launch, timeoutMs, requireReleaseIdentity) {
|
|
|
4141
4241
|
}
|
|
4142
4242
|
if (typeof response.id !== "number")
|
|
4143
4243
|
continue;
|
|
4144
|
-
const
|
|
4145
|
-
if (!
|
|
4244
|
+
const resolve11 = pending.get(response.id);
|
|
4245
|
+
if (!resolve11)
|
|
4146
4246
|
continue;
|
|
4147
4247
|
pending.delete(response.id);
|
|
4148
|
-
|
|
4248
|
+
resolve11(response);
|
|
4149
4249
|
}
|
|
4150
4250
|
});
|
|
4151
4251
|
const timeout = new Promise((_, reject) => {
|
|
@@ -4249,52 +4349,91 @@ async function verifyLaunchInternal(launch, timeoutMs, requireReleaseIdentity) {
|
|
|
4249
4349
|
}
|
|
4250
4350
|
}
|
|
4251
4351
|
|
|
4252
|
-
// src/
|
|
4253
|
-
import {
|
|
4254
|
-
import {
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
},
|
|
4288
|
-
"linux-x64": {
|
|
4289
|
-
sha256: "af97e7e267b558960156175c840c3c2d2cd7aabf68f1f72c7d64b6d50742a79e",
|
|
4290
|
-
relativePath: "asset3d/provider/asset3d-search-provider-a274238712c1dced99e4aa3148a0d15904ce2a90-linux-x64.tar.gz"
|
|
4352
|
+
// src/devkit/engine-mounts.ts
|
|
4353
|
+
import { lstatSync as lstatSync7, readFileSync as readFileSync12, readdirSync as readdirSync6, readlinkSync as readlinkSync2, rmdirSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync8 } from "node:fs";
|
|
4354
|
+
import { dirname as dirname9, join as join10, resolve as resolve11 } from "node:path";
|
|
4355
|
+
var HOSTS = {
|
|
4356
|
+
".agents/skills": ["codex"],
|
|
4357
|
+
".claude/skills": ["claude"],
|
|
4358
|
+
".cursor/skills": ["cursor"],
|
|
4359
|
+
".codebuddy/skills": ["codebuddy", "workbuddy"],
|
|
4360
|
+
".workbuddy/skills": ["workbuddy"]
|
|
4361
|
+
};
|
|
4362
|
+
function pruneUnselectedEngineMounts(root, clients) {
|
|
4363
|
+
const manifestPath = join10(root, ".forgeax", "skill-install-manifest.json");
|
|
4364
|
+
const regular = (path) => {
|
|
4365
|
+
try {
|
|
4366
|
+
const stat = lstatSync7(path);
|
|
4367
|
+
return stat.isFile() && !stat.isSymbolicLink();
|
|
4368
|
+
} catch {
|
|
4369
|
+
return false;
|
|
4370
|
+
}
|
|
4371
|
+
};
|
|
4372
|
+
const directory = (path) => {
|
|
4373
|
+
try {
|
|
4374
|
+
const stat = lstatSync7(path);
|
|
4375
|
+
return stat.isDirectory() && !stat.isSymbolicLink();
|
|
4376
|
+
} catch {
|
|
4377
|
+
return false;
|
|
4378
|
+
}
|
|
4379
|
+
};
|
|
4380
|
+
if (!directory(join10(root, ".forgeax")) || !regular(manifestPath))
|
|
4381
|
+
return [];
|
|
4382
|
+
let manifest;
|
|
4383
|
+
try {
|
|
4384
|
+
manifest = JSON.parse(readFileSync12(manifestPath, "utf8"));
|
|
4385
|
+
} catch {
|
|
4386
|
+
return [];
|
|
4291
4387
|
}
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4388
|
+
if (!manifest || manifest.schemaVersion !== "1.0.0" || manifest.sourceRoot !== "skills" || !Array.isArray(manifest.mounts))
|
|
4389
|
+
return [];
|
|
4390
|
+
const removed = [];
|
|
4391
|
+
for (const mount of manifest.mounts) {
|
|
4392
|
+
if (!mount || typeof mount.root !== "string" || !Object.hasOwn(HOSTS, mount.root))
|
|
4393
|
+
continue;
|
|
4394
|
+
const hosts = HOSTS[mount.root];
|
|
4395
|
+
if (!hosts || hosts.some((host) => clients.includes(host)) || !Array.isArray(mount.skills))
|
|
4396
|
+
continue;
|
|
4397
|
+
if (!mount.skills.length || !mount.skills.every((id) => /^forgeax-engine-[a-z0-9-]+$/.test(id)) || new Set(mount.skills).size !== mount.skills.length)
|
|
4398
|
+
continue;
|
|
4399
|
+
const path = join10(root, mount.root);
|
|
4400
|
+
if (!directory(dirname9(path)) || !directory(path))
|
|
4401
|
+
continue;
|
|
4402
|
+
const expected = [".gitignore", ...mount.skills].sort();
|
|
4403
|
+
if (JSON.stringify(readdirSync6(path).sort()) !== JSON.stringify(expected))
|
|
4404
|
+
continue;
|
|
4405
|
+
const ignore = join10(path, ".gitignore");
|
|
4406
|
+
const expectedIgnore = ["# BEGIN FORGEAX MANAGED SKILLS", ...mount.skills.map((id) => `/${id}`), "# END FORGEAX MANAGED SKILLS", ""].join(`
|
|
4407
|
+
`);
|
|
4408
|
+
if (!regular(ignore) || readFileSync12(ignore, "utf8") !== expectedIgnore)
|
|
4409
|
+
continue;
|
|
4410
|
+
if (!mount.skills.every((id) => {
|
|
4411
|
+
const link = join10(path, id);
|
|
4412
|
+
return lstatSync7(link).isSymbolicLink() && resolve11(path, readlinkSync2(link)) === resolve11(root, "skills", id);
|
|
4413
|
+
}))
|
|
4414
|
+
continue;
|
|
4415
|
+
for (const id of mount.skills)
|
|
4416
|
+
unlinkSync2(join10(path, id));
|
|
4417
|
+
unlinkSync2(ignore);
|
|
4418
|
+
rmdirSync(path);
|
|
4419
|
+
if (readdirSync6(dirname9(path)).length === 0)
|
|
4420
|
+
rmdirSync(dirname9(path));
|
|
4421
|
+
removed.push(mount.root);
|
|
4422
|
+
}
|
|
4423
|
+
if (removed.length) {
|
|
4424
|
+
manifest.mounts = manifest.mounts.filter((mount) => !mount || !removed.includes(mount.root));
|
|
4425
|
+
writeFileSync8(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
4426
|
+
`);
|
|
4427
|
+
}
|
|
4428
|
+
return removed;
|
|
4429
|
+
}
|
|
4430
|
+
|
|
4431
|
+
// src/extensions/manager.ts
|
|
4432
|
+
import { createHash as createHash6, randomUUID as randomUUID4 } from "node:crypto";
|
|
4433
|
+
import { existsSync as existsSync8, lstatSync as lstatSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync13, readdirSync as readdirSync7, realpathSync as realpathSync7, renameSync as renameSync5, rmSync as rmSync5, rmdirSync as rmdirSync2, writeFileSync as writeFileSync9 } from "node:fs";
|
|
4434
|
+
import { dirname as dirname10, isAbsolute as isAbsolute4, relative as relative8, resolve as resolve12, sep as sep5 } from "node:path";
|
|
4435
|
+
import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
|
|
4436
|
+
var mounts = {
|
|
4298
4437
|
codex: ".agents/skills",
|
|
4299
4438
|
claude: ".claude/skills",
|
|
4300
4439
|
cursor: ".cursor/skills",
|
|
@@ -4304,2124 +4443,326 @@ var SKILL_MOUNTS = Object.freeze({
|
|
|
4304
4443
|
vscode: ".vscode/skills",
|
|
4305
4444
|
zcode: ".zcode/skills",
|
|
4306
4445
|
opencode: ".config/opencode/skills"
|
|
4307
|
-
}
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
function
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
if (
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
// src/asset3d/fs.ts
|
|
4320
|
-
import { closeSync as closeSync2, fsyncSync, mkdirSync as mkdirSync8, openSync as openSync2, renameSync as renameSync5, writeFileSync as writeFileSync8 } from "node:fs";
|
|
4321
|
-
import { dirname as dirname9 } from "node:path";
|
|
4322
|
-
function ensurePrivateDir(path) {
|
|
4323
|
-
mkdirSync8(path, { recursive: true, mode: 448 });
|
|
4324
|
-
}
|
|
4325
|
-
function atomicWrite(path, data, mode = 384) {
|
|
4326
|
-
ensurePrivateDir(dirname9(path));
|
|
4327
|
-
const temp = `${path}.tmp-${process.pid}-${crypto.randomUUID()}`;
|
|
4328
|
-
const fd = openSync2(temp, "wx", mode);
|
|
4329
|
-
try {
|
|
4330
|
-
writeFileSync8(fd, data);
|
|
4331
|
-
fsyncSync(fd);
|
|
4332
|
-
} finally {
|
|
4333
|
-
closeSync2(fd);
|
|
4334
|
-
}
|
|
4335
|
-
renameSync5(temp, path);
|
|
4336
|
-
const dirFd = openSync2(dirname9(path), "r");
|
|
4337
|
-
try {
|
|
4338
|
-
fsyncSync(dirFd);
|
|
4339
|
-
} finally {
|
|
4340
|
-
closeSync2(dirFd);
|
|
4341
|
-
}
|
|
4342
|
-
}
|
|
4343
|
-
|
|
4344
|
-
// src/asset3d/origins.ts
|
|
4345
|
-
import { domainToASCII } from "node:url";
|
|
4346
|
-
function canonicalIpv4(host) {
|
|
4347
|
-
const parts = host.split(".");
|
|
4348
|
-
if (parts.length !== 4 || parts.some((part) => !/^\d{1,3}$/.test(part)))
|
|
4349
|
-
return;
|
|
4350
|
-
const values = parts.map(Number);
|
|
4351
|
-
if (values.some((value) => value > 255))
|
|
4352
|
-
throw new Error("download_origin_invalid: invalid IPv4 address");
|
|
4353
|
-
return values.join(".");
|
|
4354
|
-
}
|
|
4355
|
-
function canonicalizeOrigins(inputs) {
|
|
4356
|
-
if (inputs.length < 1 || inputs.length > 8) {
|
|
4357
|
-
throw new Error("download_origin_count_invalid: expected 1..8 --download-origin values");
|
|
4358
|
-
}
|
|
4359
|
-
const values = inputs.map((input) => {
|
|
4360
|
-
const lexical = /^(https?):\/\/(\[[0-9A-Fa-f:.]+\]|[^:/?#@]+):(\d{1,5})$/.exec(input);
|
|
4361
|
-
if (!lexical) {
|
|
4362
|
-
throw new Error("download_origin_invalid: expected exact scheme://host:port without path, query, fragment, or userinfo");
|
|
4363
|
-
}
|
|
4364
|
-
let parsed;
|
|
4446
|
+
};
|
|
4447
|
+
var reserved = new Set(["install", "init", "uninstall", "update", "use", "doctor", "preview", "devkit", "agents", "help", "version"]);
|
|
4448
|
+
var validId = (id) => /^[a-z][a-z0-9-]{0,63}$/.test(id) && !reserved.has(id);
|
|
4449
|
+
var digest = (data) => createHash6("sha256").update(data).digest("hex");
|
|
4450
|
+
function safe(root, path) {
|
|
4451
|
+
const target = resolve12(root, path);
|
|
4452
|
+
const rel = relative8(root, target);
|
|
4453
|
+
if (!rel || rel === ".." || rel.startsWith(`..${sep5}`) || isAbsolute4(rel))
|
|
4454
|
+
throw new Error("extension_path_escape");
|
|
4455
|
+
let cursor = root;
|
|
4456
|
+
for (const part of rel.split(sep5)) {
|
|
4457
|
+
cursor = resolve12(cursor, part);
|
|
4365
4458
|
try {
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
throw new Error("download_origin_invalid: http or https required");
|
|
4372
|
-
if (parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash) {
|
|
4373
|
-
throw new Error("download_origin_invalid: userinfo/path/query/fragment is forbidden");
|
|
4374
|
-
}
|
|
4375
|
-
const port = Number(lexical[3]);
|
|
4376
|
-
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
4377
|
-
throw new Error("download_origin_invalid: explicit port must be 1..65535");
|
|
4378
|
-
let host = lexical[2];
|
|
4379
|
-
if (host === "*" || host.includes("*"))
|
|
4380
|
-
throw new Error("download_origin_invalid: wildcard host is forbidden");
|
|
4381
|
-
if (host.startsWith("[") && host.endsWith("]")) {
|
|
4382
|
-
const normalized = new URL(`${parsed.protocol}//${host}:${port}`).hostname;
|
|
4383
|
-
host = normalized.startsWith("[") ? normalized.toLowerCase() : `[${normalized.toLowerCase()}]`;
|
|
4384
|
-
} else {
|
|
4385
|
-
host = canonicalIpv4(host) ?? domainToASCII(host.replace(/\.$/, "")).toLowerCase();
|
|
4386
|
-
if (!host)
|
|
4387
|
-
throw new Error("download_origin_invalid: host cannot be canonicalized");
|
|
4459
|
+
if (lstatSync8(cursor).isSymbolicLink())
|
|
4460
|
+
throw new Error("extension_symlink_not_allowed");
|
|
4461
|
+
} catch (error) {
|
|
4462
|
+
if (error.code !== "ENOENT")
|
|
4463
|
+
throw error;
|
|
4388
4464
|
}
|
|
4389
|
-
return `${lexical[1]}://${host}:${port}`;
|
|
4390
|
-
}).sort();
|
|
4391
|
-
if (new Set(values).size !== values.length)
|
|
4392
|
-
throw new Error("download_origin_duplicate: canonical duplicates are forbidden");
|
|
4393
|
-
const compactJson = canonicalJson(values);
|
|
4394
|
-
return { values, compactJson, digest: sha2562(compactJson) };
|
|
4395
|
-
}
|
|
4396
|
-
|
|
4397
|
-
// src/asset3d/install.ts
|
|
4398
|
-
var K0_FILES = [
|
|
4399
|
-
"PLATFORM.md",
|
|
4400
|
-
"PLATFORM.planning.md",
|
|
4401
|
-
"PLATFORM.implementation.md",
|
|
4402
|
-
"PLATFORM.verification.md",
|
|
4403
|
-
"reference.md",
|
|
4404
|
-
"package.json",
|
|
4405
|
-
"scripts/pack-deterministic.mjs",
|
|
4406
|
-
"scripts/validate-workflow.mjs",
|
|
4407
|
-
"scripts/workflow.test.mjs"
|
|
4408
|
-
];
|
|
4409
|
-
var SKILL_ID = "art-3d-asset-library";
|
|
4410
|
-
function packagedAsset3dProvider(relativePathInput, assetsRootInput) {
|
|
4411
|
-
const assetsRoot = realpathSync7(assetsRootInput ?? defaultAssetsRoot());
|
|
4412
|
-
const relativePath = relativePathInput.replaceAll("\\", "/");
|
|
4413
|
-
if (!relativePath || relativePath.startsWith("/") || relativePath.split("/").some((part) => part === "" || part === "." || part === "..")) {
|
|
4414
|
-
throw new Error("asset3d_bundled_provider_path_invalid");
|
|
4415
4465
|
}
|
|
4416
|
-
|
|
4417
|
-
if (!confined3(assetsRoot, candidate) || !existsSync8(candidate)) {
|
|
4418
|
-
throw new Error("asset3d_bundled_provider_missing: reinstall @forgeax/game");
|
|
4419
|
-
}
|
|
4420
|
-
const metadata = lstatSync7(candidate);
|
|
4421
|
-
if (!metadata.isFile() || metadata.isSymbolicLink()) {
|
|
4422
|
-
throw new Error("asset3d_bundled_provider_invalid: expected a regular package file");
|
|
4423
|
-
}
|
|
4424
|
-
return realpathSync7(candidate);
|
|
4466
|
+
return target;
|
|
4425
4467
|
}
|
|
4426
|
-
function
|
|
4427
|
-
|
|
4428
|
-
|
|
4468
|
+
function write(path, content) {
|
|
4469
|
+
mkdirSync8(dirname10(path), { recursive: true });
|
|
4470
|
+
const temp = `${path}.${randomUUID4()}.tmp`;
|
|
4471
|
+
writeFileSync9(temp, content, { mode: 384 });
|
|
4472
|
+
renameSync5(temp, path);
|
|
4429
4473
|
}
|
|
4430
|
-
function
|
|
4431
|
-
const
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
throw new Error("asset3d_package_assets_missing: packaged K0 artifact is unavailable");
|
|
4436
|
-
return found;
|
|
4474
|
+
function json2(path) {
|
|
4475
|
+
const bytes = readFileSync13(path);
|
|
4476
|
+
if (bytes.length > 1024 * 1024)
|
|
4477
|
+
throw new Error("extension_state_too_large");
|
|
4478
|
+
return JSON.parse(bytes.toString());
|
|
4437
4479
|
}
|
|
4438
|
-
function
|
|
4439
|
-
|
|
4480
|
+
function extensionRoot() {
|
|
4481
|
+
const here = dirname10(fileURLToPath3(import.meta.url));
|
|
4482
|
+
return [resolve12(here, "../assets/extensions"), resolve12(here, "../../extensions")].find(existsSync8) ?? resolve12(here, "../assets/extensions");
|
|
4440
4483
|
}
|
|
4441
|
-
function
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4484
|
+
function discoverExtensions(root = extensionRoot()) {
|
|
4485
|
+
if (!existsSync8(root))
|
|
4486
|
+
return [];
|
|
4487
|
+
const found = [];
|
|
4488
|
+
for (const id of readdirSync7(root)) {
|
|
4489
|
+
if (!validId(id))
|
|
4490
|
+
continue;
|
|
4491
|
+
const directory = safe(root, id);
|
|
4492
|
+
if (!lstatSync8(directory).isDirectory())
|
|
4493
|
+
continue;
|
|
4494
|
+
const value = json2(safe(directory, "extension.json"));
|
|
4495
|
+
if (value.schemaVersion !== 1 || value.id !== id || typeof value.version !== "string" || !/^\d+\.\d+\.\d+$/.test(value.version) || typeof value.cli !== "string" || !value.cli.endsWith(".mjs") || !Array.isArray(value.skills) || !value.skills.length || value.skills.some((s) => typeof s !== "string" || !/^skills\/[a-z][a-z0-9-]*$/.test(s))) {
|
|
4496
|
+
throw new Error(`extension_manifest_invalid: ${id}`);
|
|
4497
|
+
}
|
|
4498
|
+
safe(directory, value.cli);
|
|
4499
|
+
for (const skill of value.skills)
|
|
4500
|
+
safe(directory, skill + "/SKILL.md");
|
|
4501
|
+
found.push({ ...value, directory });
|
|
4445
4502
|
}
|
|
4446
|
-
return
|
|
4503
|
+
return found;
|
|
4447
4504
|
}
|
|
4448
|
-
function
|
|
4449
|
-
|
|
4505
|
+
function extensionState(root, id) {
|
|
4506
|
+
if (!validId(id))
|
|
4507
|
+
throw new Error("extension_id_invalid");
|
|
4508
|
+
return safe(root, `.forgeax/extensions/${id}`);
|
|
4509
|
+
}
|
|
4510
|
+
function installation(root, id) {
|
|
4511
|
+
const path = safe(root, `.forgeax/extensions/${id}/install.json`);
|
|
4512
|
+
if (!existsSync8(path))
|
|
4513
|
+
return;
|
|
4514
|
+
const record = json2(path);
|
|
4515
|
+
if (record.schemaVersion !== 1 || record.id !== id || !Array.isArray(record.files))
|
|
4516
|
+
throw new Error("extension_install_invalid");
|
|
4517
|
+
for (const file of record.files) {
|
|
4518
|
+
if (typeof file.path !== "string" || !/^[a-f0-9]{64}$/.test(file.sha256) || !Object.values(mounts).some((mount) => file.path.startsWith(mount + "/")) || !file.path.endsWith("/SKILL.md"))
|
|
4519
|
+
throw new Error("extension_install_invalid");
|
|
4520
|
+
safe(root, file.path);
|
|
4521
|
+
}
|
|
4522
|
+
return record;
|
|
4523
|
+
}
|
|
4524
|
+
async function load(extension) {
|
|
4525
|
+
let entry = safe(extension.directory, extension.cli);
|
|
4526
|
+
if (!existsSync8(entry) && existsSync8(entry.replace(/\.mjs$/, ".ts")))
|
|
4527
|
+
entry = entry.replace(/\.mjs$/, ".ts");
|
|
4528
|
+
const mod = await import(pathToFileURL(entry).href);
|
|
4529
|
+
if (typeof mod.check !== "function" || typeof mod.run !== "function")
|
|
4530
|
+
throw new Error("extension_cli_invalid");
|
|
4531
|
+
return mod;
|
|
4532
|
+
}
|
|
4533
|
+
function context(root, id) {
|
|
4534
|
+
return { projectRoot: root, stateDir: extensionState(root, id), packageVersion: RELEASE_IDENTITY.gameVersion };
|
|
4535
|
+
}
|
|
4536
|
+
function lock(root) {
|
|
4537
|
+
const path = safe(realpathSync7(root), ".forgeax/extension-operation.lock");
|
|
4538
|
+
mkdirSync8(dirname10(path), { recursive: true });
|
|
4450
4539
|
try {
|
|
4451
|
-
|
|
4540
|
+
mkdirSync8(path);
|
|
4452
4541
|
} catch {
|
|
4453
|
-
throw new Error("
|
|
4454
|
-
}
|
|
4455
|
-
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
|
|
4456
|
-
throw new Error("provider_bundle_invalid: bundle manifest must be an object");
|
|
4457
|
-
}
|
|
4458
|
-
return manifest;
|
|
4459
|
-
}
|
|
4460
|
-
function currentTarget() {
|
|
4461
|
-
const os = platform() === "win32" ? "windows" : platform();
|
|
4462
|
-
const cpu = arch() === "x64" || arch() === "arm64" ? arch() : arch();
|
|
4463
|
-
return `${os}-${cpu}`;
|
|
4464
|
-
}
|
|
4465
|
-
function validateBundleIdentity(manifest) {
|
|
4466
|
-
if (manifest.schema !== PROVIDER_BUNDLE_SCHEMA)
|
|
4467
|
-
throw new Error("provider_bundle_invalid: schema mismatch");
|
|
4468
|
-
if (manifest.providerCommit !== ASSET3D_PROVIDER_COMMIT)
|
|
4469
|
-
throw new Error("provider_bundle_invalid: provider commit mismatch");
|
|
4470
|
-
if (manifest.resultSchema?.sha256 !== RESULT_SCHEMA_SHA256 || manifest.receiptSchema?.sha256 !== RECEIPT_SCHEMA_SHA256) {
|
|
4471
|
-
throw new Error("provider_bundle_invalid: checked schema digest mismatch");
|
|
4472
|
-
}
|
|
4473
|
-
if (manifest.python !== ">=3.11,<3.13" || typeof manifest.entryPoint !== "string") {
|
|
4474
|
-
throw new Error("provider_bundle_invalid: runtime contract mismatch");
|
|
4475
|
-
}
|
|
4476
|
-
if (manifest.target !== currentTarget()) {
|
|
4477
|
-
throw new Error(`provider_platform_mismatch: bundle target ${String(manifest.target)} cannot run on ${currentTarget()}`);
|
|
4542
|
+
throw new Error("extension_busy: another operation or an interrupted lock requires attention");
|
|
4478
4543
|
}
|
|
4544
|
+
return () => rmdirSync2(path);
|
|
4479
4545
|
}
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4546
|
+
var userState = () => resolve12(process.env.FORGEAX_USER_STATE_DIR ?? resolve12(configuredHome(), ".forgeax"));
|
|
4547
|
+
var registryPath = () => resolve12(userState(), "extension-projects.json");
|
|
4548
|
+
function registeredProjects() {
|
|
4549
|
+
if (!existsSync8(registryPath()))
|
|
4550
|
+
return [];
|
|
4551
|
+
const value = json2(registryPath());
|
|
4552
|
+
if (!Array.isArray(value) || value.some((p) => typeof p !== "string" || !isAbsolute4(p)))
|
|
4553
|
+
throw new Error("extension_registry_invalid");
|
|
4554
|
+
return value;
|
|
4488
4555
|
}
|
|
4489
|
-
function
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4556
|
+
function register(root, enabled) {
|
|
4557
|
+
mkdirSync8(userState(), { recursive: true });
|
|
4558
|
+
safe(realpathSync7(userState()), "extension-projects.json");
|
|
4559
|
+
const projects = new Set(registeredProjects());
|
|
4560
|
+
enabled ? projects.add(root) : projects.delete(root);
|
|
4561
|
+
if (projects.size)
|
|
4562
|
+
write(registryPath(), JSON.stringify([...projects]) + `
|
|
4563
|
+
`);
|
|
4564
|
+
else if (existsSync8(registryPath()))
|
|
4565
|
+
rmSync5(registryPath());
|
|
4498
4566
|
}
|
|
4499
|
-
function
|
|
4500
|
-
const
|
|
4501
|
-
if (!/^[a-f0-9]{64}$/.test(expectedDigest) || actual !== expectedDigest) {
|
|
4502
|
-
throw new Error(`provider_bundle_digest_mismatch: expected ${expectedDigest}, got ${actual}`);
|
|
4503
|
-
}
|
|
4504
|
-
const manifest = readBundleManifest(archive);
|
|
4505
|
-
validateBundleIdentity(manifest);
|
|
4506
|
-
const cache = resolve10(cacheRoot, expectedDigest);
|
|
4507
|
-
if (validProvision(cache, expectedDigest))
|
|
4508
|
-
return { cache: realpathSync7(cache), manifest };
|
|
4509
|
-
if (existsSync8(cache))
|
|
4510
|
-
throw new Error("provider_cache_invalid: existing digest cache is incomplete or mismatched");
|
|
4511
|
-
ensurePrivateDir(cacheRoot);
|
|
4512
|
-
const stage = mkdtempSync2(join10(cacheRoot, ".provision-"));
|
|
4567
|
+
async function enableExtension(rootInput, extension, hosts, args, localEntry) {
|
|
4568
|
+
const release = lock(rootInput);
|
|
4513
4569
|
try {
|
|
4514
|
-
|
|
4515
|
-
writeFileSync9(verifier, tarOutput(archive, "verify/verify_asset3d_bundle.py"), { mode: 448, flag: "wx" });
|
|
4516
|
-
const python = choosePython(requestedPython);
|
|
4517
|
-
const result = spawnSync3(python, [verifier, "--archive", archive, "--sha256", expectedDigest, "--provision", cache, "--python", python, "--json"], {
|
|
4518
|
-
encoding: "utf8",
|
|
4519
|
-
maxBuffer: 4 * 1024 * 1024
|
|
4520
|
-
});
|
|
4521
|
-
if (result.status !== 0)
|
|
4522
|
-
throw new Error(`provider_provision_failed: ${result.stderr.trim() || result.stdout.trim() || `exit ${result.status}`}`);
|
|
4523
|
-
if (!validProvision(cache, expectedDigest))
|
|
4524
|
-
throw new Error("provider_provision_failed: verifier did not publish a complete cache");
|
|
4570
|
+
return await enableUnlocked(rootInput, extension, hosts, args, localEntry);
|
|
4525
4571
|
} finally {
|
|
4526
|
-
|
|
4527
|
-
}
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4572
|
+
release();
|
|
4573
|
+
}
|
|
4574
|
+
}
|
|
4575
|
+
async function enableUnlocked(rootInput, extension, hosts, args, localEntry) {
|
|
4576
|
+
const root = realpathSync7(rootInput);
|
|
4577
|
+
const state = extensionState(root, extension.id);
|
|
4578
|
+
const previous = installation(root, extension.id);
|
|
4579
|
+
if (!hosts.length || hosts.some((host) => !mounts[host]))
|
|
4580
|
+
throw new Error("extension_host_required: select installed agents with --ide");
|
|
4581
|
+
const command = localEntry ? `node '${realpathSync7(localEntry).replaceAll("'", "'\\''")}' ${extension.id}` : `npx -y @forgeax/game@${RELEASE_IDENTITY.gameVersion} ${extension.id}`;
|
|
4582
|
+
const files = hosts.flatMap((host) => extension.skills.map((skill) => {
|
|
4583
|
+
const content = readFileSync13(safe(extension.directory, `${skill}/SKILL.md`), "utf8").replaceAll("{{CLI}}", command);
|
|
4584
|
+
return { path: `${mounts[host]}/${skill.slice("skills/".length)}/SKILL.md`, sha256: digest(content), content };
|
|
4585
|
+
}));
|
|
4586
|
+
for (const file of files) {
|
|
4587
|
+
const target = safe(root, file.path);
|
|
4588
|
+
if (existsSync8(target) && digest(readFileSync13(target)) !== previous?.files.find((f) => f.path === file.path)?.sha256) {
|
|
4589
|
+
throw new Error("extension_skill_conflict: " + file.path);
|
|
4590
|
+
}
|
|
4536
4591
|
}
|
|
4537
|
-
|
|
4592
|
+
const cli = await load(extension);
|
|
4593
|
+
const config = await cli.check(context(root, extension.id), args);
|
|
4594
|
+
const targets = new Map;
|
|
4595
|
+
const remember = (path) => {
|
|
4596
|
+
targets.set(path, existsSync8(path) ? readFileSync13(path) : undefined);
|
|
4597
|
+
};
|
|
4598
|
+
for (const file of files)
|
|
4599
|
+
remember(safe(root, file.path));
|
|
4600
|
+
remember(safe(root, `.forgeax/extensions/${extension.id}/config.json`));
|
|
4601
|
+
remember(safe(root, `.forgeax/extensions/${extension.id}/install.json`));
|
|
4538
4602
|
try {
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4603
|
+
write(resolve12(state, "config.json"), JSON.stringify(config) + `
|
|
4604
|
+
`);
|
|
4605
|
+
for (const file of files)
|
|
4606
|
+
write(safe(root, file.path), file.content);
|
|
4607
|
+
const record = {
|
|
4608
|
+
schemaVersion: 1,
|
|
4609
|
+
id: extension.id,
|
|
4610
|
+
version: extension.version,
|
|
4611
|
+
packageVersion: RELEASE_IDENTITY.gameVersion,
|
|
4612
|
+
files: [...previous?.files.filter((f) => !files.some((n) => n.path === f.path)) ?? [], ...files.map(({ path, sha256: sha2562 }) => ({ path, sha256: sha2562 }))]
|
|
4613
|
+
};
|
|
4614
|
+
write(resolve12(state, "install.json"), JSON.stringify(record) + `
|
|
4615
|
+
`);
|
|
4616
|
+
register(root, true);
|
|
4617
|
+
} catch (error) {
|
|
4618
|
+
for (const [path, old] of targets) {
|
|
4619
|
+
if (old)
|
|
4620
|
+
write(path, old.toString());
|
|
4621
|
+
else if (existsSync8(path))
|
|
4622
|
+
rmSync5(path);
|
|
4623
|
+
}
|
|
4624
|
+
throw error;
|
|
4542
4625
|
}
|
|
4543
|
-
|
|
4544
|
-
return { cache: realpathSync7(cache), manifest };
|
|
4545
|
-
}
|
|
4546
|
-
function extractK0(tgz, member) {
|
|
4547
|
-
return tarOutput(tgz, `package/${member}`);
|
|
4626
|
+
return { enabled: true, id: extension.id, version: extension.version, skillFiles: files.length };
|
|
4548
4627
|
}
|
|
4549
|
-
function
|
|
4628
|
+
function disableExtension(rootInput, id) {
|
|
4629
|
+
const release = lock(rootInput);
|
|
4550
4630
|
try {
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
return;
|
|
4631
|
+
return disableUnlocked(rootInput, id);
|
|
4632
|
+
} finally {
|
|
4633
|
+
release();
|
|
4555
4634
|
}
|
|
4556
4635
|
}
|
|
4557
|
-
function
|
|
4558
|
-
const
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
};
|
|
4580
|
-
restore(config, record.previousConfig);
|
|
4581
|
-
restore(manifest, record.previousManifest);
|
|
4636
|
+
function disableUnlocked(rootInput, id) {
|
|
4637
|
+
const root = realpathSync7(rootInput);
|
|
4638
|
+
const previous = installation(root, id);
|
|
4639
|
+
if (!previous)
|
|
4640
|
+
return { disabled: true, id, removed: 0, backups: [] };
|
|
4641
|
+
const backups = [];
|
|
4642
|
+
let removed = 0;
|
|
4643
|
+
for (const file of previous.files) {
|
|
4644
|
+
const path = safe(root, file.path);
|
|
4645
|
+
if (existsSync8(path)) {
|
|
4646
|
+
if (digest(readFileSync13(path)) !== file.sha256) {
|
|
4647
|
+
const backup = safe(root, `.forgeax/extension-backups/${id}/${randomUUID4()}/${file.path}`);
|
|
4648
|
+
mkdirSync8(dirname10(backup), { recursive: true });
|
|
4649
|
+
renameSync5(path, backup);
|
|
4650
|
+
backups.push(backup);
|
|
4651
|
+
} else
|
|
4652
|
+
rmSync5(path);
|
|
4653
|
+
removed++;
|
|
4654
|
+
}
|
|
4655
|
+
try {
|
|
4656
|
+
rmdirSync2(dirname10(path));
|
|
4657
|
+
} catch {}
|
|
4582
4658
|
}
|
|
4583
|
-
|
|
4659
|
+
rmSync5(extensionState(root, id), { recursive: true });
|
|
4660
|
+
const installed = installedExtensions(root);
|
|
4661
|
+
if (!installed.length)
|
|
4662
|
+
register(root, false);
|
|
4663
|
+
return { disabled: true, id, removed, backups };
|
|
4584
4664
|
}
|
|
4585
|
-
function
|
|
4586
|
-
|
|
4665
|
+
function installedExtensions(root) {
|
|
4666
|
+
const dir = safe(root, ".forgeax/extensions");
|
|
4667
|
+
return existsSync8(dir) ? readdirSync7(dir).filter((id) => validId(id) && installation(root, id)) : [];
|
|
4587
4668
|
}
|
|
4588
|
-
function
|
|
4589
|
-
|
|
4590
|
-
return [];
|
|
4591
|
-
return readdirSync6(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
4592
|
-
const path = resolve10(directory, entry.name);
|
|
4593
|
-
if (entry.isSymbolicLink())
|
|
4594
|
-
throw new Error(`owned_skill_collision: symlink ${path}`);
|
|
4595
|
-
return entry.isDirectory() ? filesUnder2(root, path) : entry.isFile() ? [path] : (() => {
|
|
4596
|
-
throw new Error(`owned_skill_collision: special file ${path}`);
|
|
4597
|
-
})();
|
|
4598
|
-
});
|
|
4669
|
+
function disableAllExtensions(root) {
|
|
4670
|
+
return installedExtensions(root).map((id) => disableExtension(root, id));
|
|
4599
4671
|
}
|
|
4600
|
-
function
|
|
4601
|
-
|
|
4602
|
-
|
|
4672
|
+
async function runExtension(root, extension, args) {
|
|
4673
|
+
const release = lock(root);
|
|
4674
|
+
try {
|
|
4675
|
+
const record = installation(root, extension.id);
|
|
4676
|
+
if (!record)
|
|
4677
|
+
throw new Error("extension_not_enabled: run " + extension.id + " enable");
|
|
4678
|
+
if (record.version !== extension.version || record.packageVersion !== RELEASE_IDENTITY.gameVersion)
|
|
4679
|
+
throw new Error("extension_version_mismatch: enable with this version");
|
|
4680
|
+
return await (await load(extension)).run(context(root, extension.id), args);
|
|
4681
|
+
} finally {
|
|
4682
|
+
release();
|
|
4603
4683
|
}
|
|
4604
|
-
return {
|
|
4605
|
-
command: gamePluginLaunch.command,
|
|
4606
|
-
args: [...gamePluginLaunch.args.slice(0, -1), "asset3d", "mcp"]
|
|
4607
|
-
};
|
|
4608
4684
|
}
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4685
|
+
|
|
4686
|
+
// src/cli/dispatch.ts
|
|
4687
|
+
var HELP = `ForgeaX game development plugin
|
|
4688
|
+
|
|
4689
|
+
Usage:
|
|
4690
|
+
forgeax-game install [--ide ${CLIENT_CHOICES.join(",")}] [--local]
|
|
4691
|
+
forgeax-game uninstall [--ide ...] [--purge]
|
|
4692
|
+
forgeax-game uninstall --all-projects [--ide ...]
|
|
4693
|
+
forgeax-game <extension> enable [--ide ...] [--local] [extension options]
|
|
4694
|
+
forgeax-game <extension> disable
|
|
4695
|
+
forgeax-game <extension> <operation> [options]
|
|
4696
|
+
forgeax-game init
|
|
4697
|
+
forgeax-game use <slug>
|
|
4698
|
+
forgeax-game doctor
|
|
4699
|
+
forgeax-game preview stop [--game <slug>] [--target-dir <path>] [--json]
|
|
4700
|
+
forgeax-game devkit install
|
|
4701
|
+
forgeax-game agents update
|
|
4702
|
+
forgeax-game update [--ide ...]
|
|
4703
|
+
forgeax-game version
|
|
4704
|
+
forgeax-game help
|
|
4705
|
+
|
|
4706
|
+
With no arguments, forgeax-game runs the stdio MCP server.
|
|
4707
|
+
`;
|
|
4708
|
+
function parseInstallArgs(args) {
|
|
4709
|
+
let mode = "npx";
|
|
4710
|
+
let ids;
|
|
4711
|
+
for (let i = 0;i < args.length; i++) {
|
|
4712
|
+
const arg = args[i];
|
|
4713
|
+
if (arg === "--local") {
|
|
4714
|
+
mode = "local";
|
|
4715
|
+
continue;
|
|
4637
4716
|
}
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
original = readFileSync12(path, "utf8");
|
|
4645
|
-
try {
|
|
4646
|
-
config = JSON.parse(original);
|
|
4647
|
-
} catch {
|
|
4648
|
-
throw new Error("project_mcp_config_invalid: top-level JSON is malformed");
|
|
4717
|
+
if (arg === "--ide") {
|
|
4718
|
+
const value = args[++i];
|
|
4719
|
+
if (!value)
|
|
4720
|
+
throw new Error("--ide requires a comma-separated client list");
|
|
4721
|
+
ids = value.split(",").map((id) => id.trim()).filter(Boolean);
|
|
4722
|
+
continue;
|
|
4649
4723
|
}
|
|
4650
|
-
if (
|
|
4651
|
-
|
|
4652
|
-
}
|
|
4653
|
-
const current = config.mcpServers;
|
|
4654
|
-
if (current !== undefined && (!current || typeof current !== "object" || Array.isArray(current))) {
|
|
4655
|
-
throw new Error("project_mcp_config_invalid: mcpServers must be an object");
|
|
4656
|
-
}
|
|
4657
|
-
const servers = { ...current ?? {} };
|
|
4658
|
-
for (const name of ["forgeax", "asset3d-search"]) {
|
|
4659
|
-
const existing = servers[name];
|
|
4660
|
-
if (existing === undefined || entryDigest(existing) === entryDigest(entries[name]))
|
|
4724
|
+
if (arg.startsWith("--ide=")) {
|
|
4725
|
+
ids = arg.slice("--ide=".length).split(",").map((id) => id.trim()).filter(Boolean);
|
|
4661
4726
|
continue;
|
|
4662
|
-
const priorDigest = previous?.configEntryDigests[name];
|
|
4663
|
-
if (!replaceOwned || !priorDigest || entryDigest(existing) !== priorDigest) {
|
|
4664
|
-
throw new Error(`mcp_server_conflict: ${name} differs from the owned entry`);
|
|
4665
4727
|
}
|
|
4728
|
+
throw new Error(`unknown install option: ${arg}`);
|
|
4666
4729
|
}
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
const
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
};
|
|
4730
|
+
const selected = ids ?? [...CLIENT_IDS];
|
|
4731
|
+
if (selected.length === 0)
|
|
4732
|
+
throw new Error("--ide did not name any clients");
|
|
4733
|
+
const uniqueNames = [...new Set(selected)];
|
|
4734
|
+
const unknown = uniqueNames.filter((id) => !findClient(id));
|
|
4735
|
+
if (unknown.length) {
|
|
4736
|
+
throw new Error(`unknown client${unknown.length === 1 ? "" : "s"}: ${unknown.join(", ")}. Choose from ${CLIENT_CHOICES.join(", ")}.`);
|
|
4737
|
+
}
|
|
4738
|
+
const clients = uniqueNames.map((id) => findClient(id));
|
|
4739
|
+
return { clients: [...new Map(clients.map((client) => [client.id, client])).values()], mode };
|
|
4677
4740
|
}
|
|
4678
|
-
function
|
|
4679
|
-
const
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
for (const client of clients) {
|
|
4683
|
-
const mount = SKILL_MOUNTS[client];
|
|
4684
|
-
const destination = resolve10(projectRoot, mount, SKILL_ID);
|
|
4685
|
-
const adapter = readFileSync12(resolve10(dirname10(tgz), "adapter", "SKILL.md"));
|
|
4686
|
-
const projected = [...K0_FILES.map((file) => [`legacy/${file}`, extractK0(tgz, file)]), ["SKILL.md", adapter]];
|
|
4687
|
-
if (existsSync8(destination)) {
|
|
4688
|
-
const priorOwned = new Set(previous?.skills.filter((entry) => confined3(destination, entry.path)).map((entry) => entry.path) ?? []);
|
|
4689
|
-
const unexpected = filesUnder2(destination).filter((path) => !priorOwned.has(path));
|
|
4690
|
-
if (unexpected.length)
|
|
4691
|
-
throw new Error(`owned_skill_collision: ${unexpected[0]}`);
|
|
4692
|
-
}
|
|
4693
|
-
for (const [file, bytes] of projected) {
|
|
4694
|
-
const path = resolve10(destination, file);
|
|
4695
|
-
if (!confined3(destination, path))
|
|
4696
|
-
throw new Error("skill_path_escape");
|
|
4697
|
-
const digest = sha2562(bytes);
|
|
4698
|
-
const old = prior.get(path);
|
|
4699
|
-
if (existsSync8(path) && digestFile(path) !== digest && (!old || digestFile(path) !== old.sha256)) {
|
|
4700
|
-
throw new Error(`owned_skill_collision: ${path}`);
|
|
4701
|
-
}
|
|
4702
|
-
owned.push({ mount, path, sha256: digest, bytes: bytes.byteLength });
|
|
4703
|
-
}
|
|
4704
|
-
const allCurrent = projected.every(([file, bytes]) => {
|
|
4705
|
-
const path = resolve10(destination, file);
|
|
4706
|
-
return existsSync8(path) && digestFile(path) === sha2562(bytes);
|
|
4707
|
-
});
|
|
4708
|
-
if (allCurrent)
|
|
4709
|
-
continue;
|
|
4710
|
-
ensurePrivateDir(dirname10(destination));
|
|
4711
|
-
const stage = `${destination}.stage-${process.pid}-${crypto.randomUUID()}`;
|
|
4712
|
-
mkdirSync9(stage, { recursive: false, mode: 448 });
|
|
4713
|
-
try {
|
|
4714
|
-
for (const [file, bytes] of projected) {
|
|
4715
|
-
const target = resolve10(stage, file);
|
|
4716
|
-
mkdirSync9(dirname10(target), { recursive: true, mode: 448 });
|
|
4717
|
-
atomicWrite(target, bytes, 384);
|
|
4718
|
-
}
|
|
4719
|
-
const backup = `${destination}.replace-${process.pid}-${crypto.randomUUID()}`;
|
|
4720
|
-
if (existsSync8(destination))
|
|
4721
|
-
renameSync6(destination, backup);
|
|
4722
|
-
try {
|
|
4723
|
-
renameSync6(stage, destination);
|
|
4724
|
-
} catch (error) {
|
|
4725
|
-
if (existsSync8(backup))
|
|
4726
|
-
renameSync6(backup, destination);
|
|
4727
|
-
throw error;
|
|
4728
|
-
}
|
|
4729
|
-
if (existsSync8(backup))
|
|
4730
|
-
rmSync5(backup, { recursive: true, force: true });
|
|
4731
|
-
changed = true;
|
|
4732
|
-
} finally {
|
|
4733
|
-
if (existsSync8(stage))
|
|
4734
|
-
rmSync5(stage, { recursive: true, force: true });
|
|
4735
|
-
}
|
|
4741
|
+
function requireProject() {
|
|
4742
|
+
const project = resolveProject();
|
|
4743
|
+
if (!project.root) {
|
|
4744
|
+
throw new Error(`no released Engine game found searching upward from ${project.searchedFrom}; run this command inside a game created by the released Engine SDK`);
|
|
4736
4745
|
}
|
|
4737
|
-
return
|
|
4738
|
-
}
|
|
4739
|
-
async function verifyProviderMcp(entry) {
|
|
4740
|
-
const child = spawn3(entry.command, [...entry.args], { stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, ...entry.env } });
|
|
4741
|
-
let buffer = "";
|
|
4742
|
-
let stderr = "";
|
|
4743
|
-
let protocolError;
|
|
4744
|
-
const responses = new Map;
|
|
4745
|
-
child.stdout.setEncoding("utf8");
|
|
4746
|
-
child.stdout.on("data", (chunk) => {
|
|
4747
|
-
buffer += chunk;
|
|
4748
|
-
for (;; ) {
|
|
4749
|
-
const newline = buffer.indexOf(`
|
|
4750
|
-
`);
|
|
4751
|
-
if (newline < 0)
|
|
4752
|
-
break;
|
|
4753
|
-
const line = buffer.slice(0, newline).trim();
|
|
4754
|
-
buffer = buffer.slice(newline + 1);
|
|
4755
|
-
try {
|
|
4756
|
-
const value = JSON.parse(line);
|
|
4757
|
-
if (typeof value.id === "number")
|
|
4758
|
-
responses.get(value.id)?.(value);
|
|
4759
|
-
} catch {
|
|
4760
|
-
protocolError = "provider_mcp_stdout_invalid";
|
|
4761
|
-
}
|
|
4762
|
-
}
|
|
4763
|
-
});
|
|
4764
|
-
child.stderr.setEncoding("utf8");
|
|
4765
|
-
child.stderr.on("data", (chunk) => {
|
|
4766
|
-
stderr = `${stderr}${chunk}`.slice(-8192);
|
|
4767
|
-
});
|
|
4768
|
-
const request2 = (id, method, params = {}) => new Promise((resolveResponse, reject) => {
|
|
4769
|
-
responses.set(id, resolveResponse);
|
|
4770
|
-
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}
|
|
4771
|
-
`, (error) => error && reject(error));
|
|
4772
|
-
});
|
|
4773
|
-
const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error("provider_mcp_handshake_timeout")), 8000));
|
|
4774
|
-
try {
|
|
4775
|
-
const initialized = await Promise.race([request2(1, "initialize", { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "forgeax-game-installer", version: "1" } }), timeout]);
|
|
4776
|
-
if (protocolError)
|
|
4777
|
-
throw new Error(protocolError);
|
|
4778
|
-
if (initialized.error)
|
|
4779
|
-
throw new Error("provider_mcp_initialize_failed");
|
|
4780
|
-
const listed = await Promise.race([request2(2, "tools/list"), timeout]);
|
|
4781
|
-
if (protocolError)
|
|
4782
|
-
throw new Error(protocolError);
|
|
4783
|
-
const tools = (listed.result?.tools ?? []).map((tool) => tool.name);
|
|
4784
|
-
if (!tools.includes("search_asset"))
|
|
4785
|
-
throw new Error("provider_mcp_tool_missing: search_asset");
|
|
4786
|
-
} catch (error) {
|
|
4787
|
-
throw new Error(`${error instanceof Error ? error.message : String(error)}${stderr.trim() ? `: ${stderr.trim()}` : ""}`);
|
|
4788
|
-
} finally {
|
|
4789
|
-
child.stdin.end();
|
|
4790
|
-
if (child.exitCode === null && child.signalCode === null)
|
|
4791
|
-
child.kill("SIGTERM");
|
|
4792
|
-
}
|
|
4793
|
-
}
|
|
4794
|
-
async function publishAsset3d(options, provisioned) {
|
|
4795
|
-
const projectRoot = realpathSync7(options.projectRoot);
|
|
4796
|
-
const forgeax = resolve10(projectRoot, ".forgeax");
|
|
4797
|
-
ensurePrivateDir(forgeax);
|
|
4798
|
-
chmodSync4(forgeax, 448);
|
|
4799
|
-
recoverInstallPublication(forgeax);
|
|
4800
|
-
const origins = canonicalizeOrigins(options.downloadOrigins);
|
|
4801
|
-
const catalogBaseUrl = options.catalogBaseUrl ? canonicalizeOrigins([options.catalogBaseUrl]).values[0] : undefined;
|
|
4802
|
-
if (catalogBaseUrl && !origins.values.includes(catalogBaseUrl)) {
|
|
4803
|
-
throw new Error("catalog_origin_not_authorized: --catalog-base-url must also be a --download-origin");
|
|
4804
|
-
}
|
|
4805
|
-
if (Boolean(options.awApiBaseUrl) !== Boolean(options.awCredentialFile)) {
|
|
4806
|
-
throw new Error("asset3d_aw_config_invalid: AW API base URL and credential file must be configured together");
|
|
4807
|
-
}
|
|
4808
|
-
if (options.awDepotName && !options.awApiBaseUrl) {
|
|
4809
|
-
throw new Error("asset3d_aw_config_invalid: depot name requires an AW API base URL");
|
|
4810
|
-
}
|
|
4811
|
-
const awApiBaseUrl = options.awApiBaseUrl;
|
|
4812
|
-
const awDepotName = awApiBaseUrl ? options.awDepotName ?? "aw" : undefined;
|
|
4813
|
-
const awCredentialFile = options.awCredentialFile ? resolve10(options.awCredentialFile) : undefined;
|
|
4814
|
-
if (awCredentialFile && !isAbsolute3(options.awCredentialFile)) {
|
|
4815
|
-
throw new Error("asset3d_credential_path_invalid: absolute path required");
|
|
4816
|
-
}
|
|
4817
|
-
const assetsRoot = options.assetsRoot ?? defaultAssetsRoot();
|
|
4818
|
-
const asset3dAssets = existsSync8(resolve10(assetsRoot, "asset3d", "vibegame-art-3d-asset-library-2.0.0.tgz")) ? resolve10(assetsRoot, "asset3d") : assetsRoot;
|
|
4819
|
-
const k0 = resolve10(asset3dAssets, "vibegame-art-3d-asset-library-2.0.0.tgz");
|
|
4820
|
-
if (digestFile(k0) !== K0_PACKAGE_SHA256)
|
|
4821
|
-
throw new Error("k0_package_digest_mismatch");
|
|
4822
|
-
const manifestPath = resolve10(forgeax, "asset3d-install.json");
|
|
4823
|
-
const previous = readInstallManifest(manifestPath);
|
|
4824
|
-
const gamePluginLaunch = options.gamePluginLaunch ?? {
|
|
4825
|
-
command: realpathSync7(options.executable ?? process.argv[1]),
|
|
4826
|
-
args: ["mcp"]
|
|
4827
|
-
};
|
|
4828
|
-
const entries = launchEntries(projectRoot, provisioned.cache, origins, gamePluginLaunch, catalogBaseUrl, awApiBaseUrl, awDepotName, awCredentialFile);
|
|
4829
|
-
if (options.verifyMcp !== false) {
|
|
4830
|
-
await verifyProviderMcp({
|
|
4831
|
-
...entries["asset3d-search"],
|
|
4832
|
-
command: resolve10(provisioned.cache, "bin", "asset3d-search"),
|
|
4833
|
-
args: []
|
|
4834
|
-
});
|
|
4835
|
-
}
|
|
4836
|
-
const configPath = resolve10(forgeax, "mcp.json");
|
|
4837
|
-
const merged = mergeConfig(configPath, entries, previous, options.replaceOwned === true);
|
|
4838
|
-
const clients = [...new Set(options.clients ?? Object.keys(SKILL_MOUNTS))];
|
|
4839
|
-
const skills = installSkills(projectRoot, k0, clients, previous);
|
|
4840
|
-
const installManifest = {
|
|
4841
|
-
schemaVersion: INSTALL_SCHEMA,
|
|
4842
|
-
providerBundleDigest: options.expectedSha256,
|
|
4843
|
-
providerCommit: provisioned.manifest.providerCommit,
|
|
4844
|
-
providerCache: provisioned.cache,
|
|
4845
|
-
k0TgzDigest: K0_PACKAGE_SHA256,
|
|
4846
|
-
originSetDigest: origins.digest,
|
|
4847
|
-
configEntryDigests: merged.digests,
|
|
4848
|
-
skills: skills.owned
|
|
4849
|
-
};
|
|
4850
|
-
const manifestBytes = `${JSON.stringify(installManifest, null, 2)}
|
|
4851
|
-
`;
|
|
4852
|
-
const unchanged = previous !== undefined && canonicalJson(previous) === canonicalJson(installManifest) && !merged.changed && !skills.changed;
|
|
4853
|
-
if (!unchanged) {
|
|
4854
|
-
const journal = resolve10(forgeax, "asset3d-install.journal.json");
|
|
4855
|
-
const previousConfig = existsSync8(configPath);
|
|
4856
|
-
const previousManifest = existsSync8(manifestPath);
|
|
4857
|
-
atomicWrite(journal, `${JSON.stringify({ schemaVersion: 1, state: "publishing", configDigest: sha2562(merged.bytes), manifestDigest: sha2562(manifestBytes), previousConfig, previousManifest })}
|
|
4858
|
-
`);
|
|
4859
|
-
if (existsSync8(configPath))
|
|
4860
|
-
copyFileSync3(configPath, `${configPath}.bak.latest`);
|
|
4861
|
-
if (existsSync8(manifestPath))
|
|
4862
|
-
copyFileSync3(manifestPath, `${manifestPath}.bak.latest`);
|
|
4863
|
-
if (merged.changed)
|
|
4864
|
-
atomicWrite(configPath, merged.bytes);
|
|
4865
|
-
atomicWrite(manifestPath, manifestBytes);
|
|
4866
|
-
unlinkSync2(journal);
|
|
4867
|
-
}
|
|
4868
|
-
return { changed: !unchanged, providerCache: provisioned.cache, providerCommit: provisioned.manifest.providerCommit, originSetDigest: origins.digest, configPath, skillFiles: skills.owned.length };
|
|
4869
|
-
}
|
|
4870
|
-
async function installAsset3d(options) {
|
|
4871
|
-
if (/^https?:/i.test(options.providerBundle))
|
|
4872
|
-
throw new Error("asset3d_remote_artifact_unsupported");
|
|
4873
|
-
const archive = realpathSync7(options.providerBundle);
|
|
4874
|
-
const cacheRoot = resolve10(options.cacheRoot ?? join10(homedir3(), ".forgeax", "providers", "asset3d-search"));
|
|
4875
|
-
return publishAsset3d(options, provisionBundle(archive, options.expectedSha256, cacheRoot, options.python));
|
|
4876
|
-
}
|
|
4877
|
-
function prepareAsset3dProvider(options) {
|
|
4878
|
-
if (/^https?:/i.test(options.providerBundle))
|
|
4879
|
-
throw new Error("asset3d_remote_artifact_unsupported");
|
|
4880
|
-
const archive = realpathSync7(options.providerBundle);
|
|
4881
|
-
const cacheRoot = resolve10(options.cacheRoot ?? join10(homedir3(), ".forgeax", "providers", "asset3d-search"));
|
|
4882
|
-
return provisionBundle(archive, options.expectedSha256, cacheRoot, options.python).cache;
|
|
4883
|
-
}
|
|
4884
|
-
async function installProvisionedAsset3d(options) {
|
|
4885
|
-
return publishAsset3d(options, useProvisionedCache(options.providerCache, options.expectedSha256));
|
|
4886
|
-
}
|
|
4887
|
-
function asset3dProviderLaunch(projectRootInput) {
|
|
4888
|
-
const projectRoot = realpathSync7(projectRootInput);
|
|
4889
|
-
const manifest = readInstallManifest(resolve10(projectRoot, ".forgeax", "asset3d-install.json"));
|
|
4890
|
-
if (!manifest)
|
|
4891
|
-
throw new Error("asset3d_not_installed");
|
|
4892
|
-
let config;
|
|
4893
|
-
try {
|
|
4894
|
-
config = JSON.parse(readFileSync12(resolve10(projectRoot, ".forgeax", "mcp.json"), "utf8"));
|
|
4895
|
-
} catch {
|
|
4896
|
-
throw new Error("project_mcp_config_invalid");
|
|
4897
|
-
}
|
|
4898
|
-
const entry = config?.mcpServers?.["asset3d-search"];
|
|
4899
|
-
if (!entry || typeof entry !== "object" || Array.isArray(entry) || entryDigest(entry) !== manifest.configEntryDigests["asset3d-search"]) {
|
|
4900
|
-
throw new Error("asset3d_provider_config_unowned");
|
|
4901
|
-
}
|
|
4902
|
-
const launch = entry;
|
|
4903
|
-
const expectedCommand = resolve10(manifest.providerCache, "bin", "asset3d-search");
|
|
4904
|
-
if (!existsSync8(expectedCommand) || !launch.env || typeof launch.env !== "object" || Array.isArray(launch.env)) {
|
|
4905
|
-
throw new Error("asset3d_provider_config_invalid");
|
|
4906
|
-
}
|
|
4907
|
-
const env3 = Object.fromEntries(Object.entries(launch.env).map(([name, value]) => {
|
|
4908
|
-
if (typeof value !== "string")
|
|
4909
|
-
throw new Error("asset3d_provider_config_invalid");
|
|
4910
|
-
return [name, value];
|
|
4911
|
-
}));
|
|
4912
|
-
return {
|
|
4913
|
-
command: expectedCommand,
|
|
4914
|
-
args: [],
|
|
4915
|
-
env: env3
|
|
4916
|
-
};
|
|
4917
|
-
}
|
|
4918
|
-
function uninstallAsset3d(projectRootInput) {
|
|
4919
|
-
const projectRoot = realpathSync7(projectRootInput);
|
|
4920
|
-
const forgeax = resolve10(projectRoot, ".forgeax");
|
|
4921
|
-
const manifestPath = resolve10(forgeax, "asset3d-install.json");
|
|
4922
|
-
const manifest = readInstallManifest(manifestPath);
|
|
4923
|
-
if (!manifest)
|
|
4924
|
-
return { removed: 0, modified: [] };
|
|
4925
|
-
const modified = [];
|
|
4926
|
-
let removed = 0;
|
|
4927
|
-
for (const skill of manifest.skills) {
|
|
4928
|
-
if (!existsSync8(skill.path))
|
|
4929
|
-
continue;
|
|
4930
|
-
if (digestFile(skill.path) !== skill.sha256) {
|
|
4931
|
-
modified.push(skill.path);
|
|
4932
|
-
continue;
|
|
4933
|
-
}
|
|
4934
|
-
unlinkSync2(skill.path);
|
|
4935
|
-
removed++;
|
|
4936
|
-
}
|
|
4937
|
-
const configPath = resolve10(forgeax, "mcp.json");
|
|
4938
|
-
if (existsSync8(configPath)) {
|
|
4939
|
-
const config = JSON.parse(readFileSync12(configPath, "utf8"));
|
|
4940
|
-
const servers = config.mcpServers;
|
|
4941
|
-
if (servers && typeof servers === "object" && !Array.isArray(servers)) {
|
|
4942
|
-
const next = { ...servers };
|
|
4943
|
-
for (const name of ["forgeax", "asset3d-search"]) {
|
|
4944
|
-
if (next[name] !== undefined && entryDigest(next[name]) === manifest.configEntryDigests[name]) {
|
|
4945
|
-
delete next[name];
|
|
4946
|
-
removed++;
|
|
4947
|
-
} else if (next[name] !== undefined)
|
|
4948
|
-
modified.push(`mcpServers.${name}`);
|
|
4949
|
-
}
|
|
4950
|
-
atomicWrite(configPath, `${JSON.stringify({ ...config, mcpServers: next }, null, 2)}
|
|
4951
|
-
`);
|
|
4952
|
-
}
|
|
4953
|
-
}
|
|
4954
|
-
if (modified.length === 0)
|
|
4955
|
-
unlinkSync2(manifestPath);
|
|
4956
|
-
return { removed, modified };
|
|
4957
|
-
}
|
|
4958
|
-
|
|
4959
|
-
// src/asset3d/transaction.ts
|
|
4960
|
-
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
4961
|
-
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
4962
|
-
import {
|
|
4963
|
-
closeSync as closeSync3,
|
|
4964
|
-
constants,
|
|
4965
|
-
cpSync as cpSync2,
|
|
4966
|
-
existsSync as existsSync9,
|
|
4967
|
-
fstatSync,
|
|
4968
|
-
lstatSync as lstatSync9,
|
|
4969
|
-
mkdirSync as mkdirSync10,
|
|
4970
|
-
openSync as openSync4,
|
|
4971
|
-
readFileSync as readFileSync14,
|
|
4972
|
-
readdirSync as readdirSync7,
|
|
4973
|
-
realpathSync as realpathSync9,
|
|
4974
|
-
renameSync as renameSync7,
|
|
4975
|
-
rmSync as rmSync6,
|
|
4976
|
-
statSync as statSync6,
|
|
4977
|
-
unlinkSync as unlinkSync3,
|
|
4978
|
-
writeFileSync as writeFileSync10
|
|
4979
|
-
} from "node:fs";
|
|
4980
|
-
import { isAbsolute as isAbsolute5, relative as relative10, resolve as resolve12, sep as sep7 } from "node:path";
|
|
4981
|
-
|
|
4982
|
-
// src/asset3d/schema.ts
|
|
4983
|
-
var ID = /^[A-Za-z0-9._-]{1,128}$/;
|
|
4984
|
-
var DIGEST = /^[a-f0-9]{64}$/;
|
|
4985
|
-
var ERROR_CODES = new Set([
|
|
4986
|
-
"asset_not_found",
|
|
4987
|
-
"asset_identity_missing",
|
|
4988
|
-
"search_timeout",
|
|
4989
|
-
"search_upstream_error",
|
|
4990
|
-
"download_timeout",
|
|
4991
|
-
"download_origin_rejected",
|
|
4992
|
-
"download_too_large",
|
|
4993
|
-
"archive_rejected",
|
|
4994
|
-
"conversion_failed",
|
|
4995
|
-
"digest_failed",
|
|
4996
|
-
"batch_timeout",
|
|
4997
|
-
"internal_error"
|
|
4998
|
-
]);
|
|
4999
|
-
var ROLES = new Set(["primary-pack", "auxiliary-pack", "primary-model", "animation", "auxiliary-model", "texture", "metadata"]);
|
|
5000
|
-
function exactKeys(record, allowed, field) {
|
|
5001
|
-
const extras = Object.keys(record).filter((key) => !allowed.includes(key));
|
|
5002
|
-
if (extras.length)
|
|
5003
|
-
throw new Error(`provider_result_invalid: unknown ${field} fields`);
|
|
5004
|
-
}
|
|
5005
|
-
function safeRelativePath(value) {
|
|
5006
|
-
if (typeof value !== "string" || value.length < 1 || value.length > 512 || value.startsWith("/") || value.includes("\\")) {
|
|
5007
|
-
throw new Error("provider_result_invalid: manifest path must be relative POSIX");
|
|
5008
|
-
}
|
|
5009
|
-
const parts = value.split("/");
|
|
5010
|
-
if (parts.some((part) => !part || part === "." || part === ".."))
|
|
5011
|
-
throw new Error("provider_result_invalid: unsafe manifest path");
|
|
5012
|
-
return value;
|
|
5013
|
-
}
|
|
5014
|
-
function number(value, min, max, field) {
|
|
5015
|
-
if (!Number.isInteger(value) || value < min || value > max)
|
|
5016
|
-
throw new Error(`provider_result_invalid: ${field}`);
|
|
5017
|
-
return value;
|
|
5018
|
-
}
|
|
5019
|
-
function text(value, min, max, field) {
|
|
5020
|
-
if (typeof value !== "string" || [...value].length < min || [...value].length > max)
|
|
5021
|
-
throw new Error(`provider_result_invalid: ${field}`);
|
|
5022
|
-
return value;
|
|
5023
|
-
}
|
|
5024
|
-
function aggregate(entries) {
|
|
5025
|
-
const bytes = entries.map((entry) => `${entry.path}\x00${entry.bytes}\x00${entry.sha256}
|
|
5026
|
-
`).join("");
|
|
5027
|
-
return sha2562(bytes);
|
|
5028
|
-
}
|
|
5029
|
-
function parseProviderResult(input, expectedCommit, expectedOriginSetDigest) {
|
|
5030
|
-
const bytes = typeof input === "string" ? Buffer.byteLength(input) : input.byteLength;
|
|
5031
|
-
if (bytes > MAX_JSON_BYTES)
|
|
5032
|
-
throw new Error("provider_result_too_large: stdin exceeds 1 MiB");
|
|
5033
|
-
let raw;
|
|
5034
|
-
try {
|
|
5035
|
-
raw = JSON.parse(typeof input === "string" ? input : Buffer.from(input).toString("utf8"));
|
|
5036
|
-
} catch {
|
|
5037
|
-
throw new Error("provider_result_invalid: stdin is not JSON");
|
|
5038
|
-
}
|
|
5039
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
5040
|
-
throw new Error("provider_result_invalid: top-level object required");
|
|
5041
|
-
const root = raw;
|
|
5042
|
-
exactKeys(root, ["schemaVersion", "total", "succeeded", "failed", "results", "receipt"], "top-level");
|
|
5043
|
-
if (root.schemaVersion !== PROVIDER_RESULT_SCHEMA)
|
|
5044
|
-
throw new Error("provider_result_invalid: schemaVersion");
|
|
5045
|
-
const total = number(root.total, 1, 16, "total");
|
|
5046
|
-
const succeeded = number(root.succeeded, 0, total, "succeeded");
|
|
5047
|
-
const failed = number(root.failed, 0, total, "failed");
|
|
5048
|
-
if (succeeded + failed !== total || !Array.isArray(root.results) || root.results.length !== total)
|
|
5049
|
-
throw new Error("provider_result_invalid: counts");
|
|
5050
|
-
let receipt;
|
|
5051
|
-
if (root.receipt !== undefined) {
|
|
5052
|
-
if (!root.receipt || typeof root.receipt !== "object" || Array.isArray(root.receipt)) {
|
|
5053
|
-
throw new Error("provider_result_invalid: receipt object required");
|
|
5054
|
-
}
|
|
5055
|
-
const value = root.receipt;
|
|
5056
|
-
exactKeys(value, ["schemaVersion", "provider", "providerCommit", "originSetDigest"], "receipt");
|
|
5057
|
-
if (value.schemaVersion !== PROVIDER_RECEIPT_SCHEMA || value.provider !== "ea-3d" || value.providerCommit !== expectedCommit || value.originSetDigest !== expectedOriginSetDigest) {
|
|
5058
|
-
throw new Error("provider_result_invalid: receipt identity");
|
|
5059
|
-
}
|
|
5060
|
-
receipt = {
|
|
5061
|
-
schemaVersion: PROVIDER_RECEIPT_SCHEMA,
|
|
5062
|
-
provider: "ea-3d",
|
|
5063
|
-
providerCommit: expectedCommit,
|
|
5064
|
-
originSetDigest: expectedOriginSetDigest
|
|
5065
|
-
};
|
|
5066
|
-
}
|
|
5067
|
-
if (succeeded > 0 && receipt === undefined)
|
|
5068
|
-
throw new Error("provider_result_invalid: success receipt required");
|
|
5069
|
-
const indices = new Set;
|
|
5070
|
-
let okCount = 0;
|
|
5071
|
-
const results = root.results.map((candidate) => {
|
|
5072
|
-
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
|
|
5073
|
-
throw new Error("provider_result_invalid: result row");
|
|
5074
|
-
const row = candidate;
|
|
5075
|
-
const queryIndex = number(row.queryIndex, 0, total - 1, "queryIndex");
|
|
5076
|
-
if (indices.has(queryIndex))
|
|
5077
|
-
throw new Error("provider_result_invalid: duplicate queryIndex");
|
|
5078
|
-
indices.add(queryIndex);
|
|
5079
|
-
const query = text(row.query, 1, 200, "query");
|
|
5080
|
-
if (row.status === "error") {
|
|
5081
|
-
exactKeys(row, ["status", "queryIndex", "query", "code", "retryable", "message"], "error");
|
|
5082
|
-
if (typeof row.code !== "string" || !ERROR_CODES.has(row.code) || typeof row.retryable !== "boolean")
|
|
5083
|
-
throw new Error("provider_result_invalid: error row");
|
|
5084
|
-
const message = text(row.message, 0, 256, "message");
|
|
5085
|
-
return { status: "error", queryIndex, query, code: row.code, retryable: row.retryable, message };
|
|
5086
|
-
}
|
|
5087
|
-
if (row.status !== "ok")
|
|
5088
|
-
throw new Error("provider_result_invalid: status");
|
|
5089
|
-
const isPack = row.deliveredFormat === "pack";
|
|
5090
|
-
const primaryKey = isPack ? "primaryPack" : "primaryModel";
|
|
5091
|
-
const primaryRole = isPack ? "primary-pack" : "primary-model";
|
|
5092
|
-
exactKeys(row, ["status", "queryIndex", "query", "provider", "providerAssetId", "assetName", "deliveredFormat", "sha256", "bytes", primaryKey, "manifest", "originSetDigest", "downloaded_to"], "success");
|
|
5093
|
-
okCount++;
|
|
5094
|
-
if (row.provider !== "ea-3d" || row.deliveredFormat !== "glb" && !isPack || typeof row.providerAssetId !== "string" || !ID.test(row.providerAssetId) || row.providerAssetId === "." || row.providerAssetId === "..") {
|
|
5095
|
-
throw new Error("provider_result_invalid: success identity");
|
|
5096
|
-
}
|
|
5097
|
-
const assetName = text(row.assetName, 1, 128, "assetName");
|
|
5098
|
-
const itemBytes = number(row.bytes, 1, 268435456, "bytes");
|
|
5099
|
-
if (typeof row.sha256 !== "string" || !DIGEST.test(row.sha256))
|
|
5100
|
-
throw new Error("provider_result_invalid: aggregate digest");
|
|
5101
|
-
if (!Array.isArray(row.manifest) || row.manifest.length < 1 || row.manifest.length > 1024)
|
|
5102
|
-
throw new Error("provider_result_invalid: manifest count");
|
|
5103
|
-
const seen = new Set;
|
|
5104
|
-
const manifest = row.manifest.map((entry) => {
|
|
5105
|
-
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
5106
|
-
throw new Error("provider_result_invalid: manifest entry");
|
|
5107
|
-
const item = entry;
|
|
5108
|
-
exactKeys(item, ["path", "role", "bytes", "sha256"], "manifest");
|
|
5109
|
-
const path = safeRelativePath(item.path);
|
|
5110
|
-
if (seen.has(path))
|
|
5111
|
-
throw new Error("provider_result_invalid: duplicate manifest path");
|
|
5112
|
-
seen.add(path);
|
|
5113
|
-
if (typeof item.role !== "string" || !ROLES.has(item.role))
|
|
5114
|
-
throw new Error("provider_result_invalid: manifest role");
|
|
5115
|
-
const role = item.role;
|
|
5116
|
-
const entryBytes = number(item.bytes, 1, 134217728, "manifest bytes");
|
|
5117
|
-
if (typeof item.sha256 !== "string" || !DIGEST.test(item.sha256))
|
|
5118
|
-
throw new Error("provider_result_invalid: manifest digest");
|
|
5119
|
-
if ((role === "primary-model" || role === "animation" || role === "auxiliary-model") && !path.toLowerCase().endsWith(".glb")) {
|
|
5120
|
-
throw new Error("provider_result_invalid: model entry must be GLB");
|
|
5121
|
-
}
|
|
5122
|
-
if ((role === "primary-pack" || role === "auxiliary-pack") && (!isPack || !/\.pack\.(json|ts)$/i.test(path))) {
|
|
5123
|
-
throw new Error("provider_result_invalid: pack entry must be authored Pack");
|
|
5124
|
-
}
|
|
5125
|
-
if (isPack && role === "primary-model")
|
|
5126
|
-
throw new Error("provider_result_invalid: multiple primary formats");
|
|
5127
|
-
return { path, role, bytes: entryBytes, sha256: item.sha256 };
|
|
5128
|
-
});
|
|
5129
|
-
const sorted = [...manifest].sort((left, right) => Buffer.from(left.path).compare(Buffer.from(right.path)));
|
|
5130
|
-
if (manifest.some((entry, index) => entry.path !== sorted[index].path))
|
|
5131
|
-
throw new Error("provider_result_invalid: manifest must be UTF-8 path sorted");
|
|
5132
|
-
const primary = manifest.filter((entry) => entry.role === primaryRole);
|
|
5133
|
-
if (primary.length !== 1 || row[primaryKey] !== primary[0].path)
|
|
5134
|
-
throw new Error("provider_result_invalid: primary model");
|
|
5135
|
-
if (manifest.reduce((sum, entry) => sum + entry.bytes, 0) !== itemBytes || aggregate(manifest) !== row.sha256) {
|
|
5136
|
-
throw new Error("provider_result_invalid: byte or aggregate digest mismatch");
|
|
5137
|
-
}
|
|
5138
|
-
if (row.originSetDigest !== expectedOriginSetDigest)
|
|
5139
|
-
throw new Error("provider_result_invalid: originSetDigest identity");
|
|
5140
|
-
return {
|
|
5141
|
-
status: "ok",
|
|
5142
|
-
queryIndex,
|
|
5143
|
-
query,
|
|
5144
|
-
provider: "ea-3d",
|
|
5145
|
-
providerAssetId: row.providerAssetId,
|
|
5146
|
-
assetName,
|
|
5147
|
-
sha256: row.sha256,
|
|
5148
|
-
bytes: itemBytes,
|
|
5149
|
-
...isPack ? { deliveredFormat: "pack", primaryPack: row.primaryPack } : { deliveredFormat: "glb", primaryModel: row.primaryModel },
|
|
5150
|
-
manifest,
|
|
5151
|
-
originSetDigest: expectedOriginSetDigest,
|
|
5152
|
-
...row.downloaded_to === undefined ? {} : { downloaded_to: row.downloaded_to }
|
|
5153
|
-
};
|
|
5154
|
-
});
|
|
5155
|
-
if (okCount !== succeeded || total - okCount !== failed)
|
|
5156
|
-
throw new Error("provider_result_invalid: status counts");
|
|
5157
|
-
return { schemaVersion: PROVIDER_RESULT_SCHEMA, total, succeeded, failed, results, ...receipt ? { receipt } : {} };
|
|
5158
|
-
}
|
|
5159
|
-
|
|
5160
|
-
// src/asset3d/pack-readback.ts
|
|
5161
|
-
import { lstatSync as lstatSync8, readFileSync as readFileSync13, realpathSync as realpathSync8 } from "node:fs";
|
|
5162
|
-
import { isAbsolute as isAbsolute4, relative as relative9, resolve as resolve11, sep as sep6 } from "node:path";
|
|
5163
|
-
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
5164
|
-
function object(value) {
|
|
5165
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
5166
|
-
throw new Error("engine_pack_readback_invalid");
|
|
5167
|
-
return value;
|
|
5168
|
-
}
|
|
5169
|
-
function readPackBuildCatalog(projectRoot, sourcePaths, buildValue) {
|
|
5170
|
-
const build = object(buildValue);
|
|
5171
|
-
const runtime = object(build.runtime);
|
|
5172
|
-
if (build.schemaVersion !== "1.0.0" || !Array.isArray(build.artifacts) || build.artifacts.length > 1e4) {
|
|
5173
|
-
throw new Error("engine_pack_build_manifest_invalid");
|
|
5174
|
-
}
|
|
5175
|
-
const dist = resolve11(projectRoot, "dist");
|
|
5176
|
-
const canonicalDist = realpathSync8(dist);
|
|
5177
|
-
if (canonicalDist !== resolve11(realpathSync8(projectRoot), "dist") || lstatSync8(dist).isSymbolicLink()) {
|
|
5178
|
-
throw new Error("engine_pack_build_path_escape");
|
|
5179
|
-
}
|
|
5180
|
-
const artifacts = new Map;
|
|
5181
|
-
let total = 0;
|
|
5182
|
-
for (const raw of build.artifacts) {
|
|
5183
|
-
const entry = object(raw);
|
|
5184
|
-
const path = safeRelativePath(entry.path);
|
|
5185
|
-
if (artifacts.has(path) || !Number.isSafeInteger(entry.bytes) || entry.bytes < 0 || entry.bytes > 256 * 1024 * 1024 || typeof entry.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(entry.sha256)) {
|
|
5186
|
-
throw new Error("engine_pack_build_manifest_invalid");
|
|
5187
|
-
}
|
|
5188
|
-
total += entry.bytes;
|
|
5189
|
-
if (total > 1024 * 1024 * 1024)
|
|
5190
|
-
throw new Error("engine_pack_build_too_large");
|
|
5191
|
-
const file = resolve11(dist, path);
|
|
5192
|
-
const rel = relative9(canonicalDist, realpathSync8(file));
|
|
5193
|
-
if (isAbsolute4(rel) || rel === ".." || rel.startsWith(`..${sep6}`))
|
|
5194
|
-
throw new Error("engine_pack_build_path_escape");
|
|
5195
|
-
const stat = lstatSync8(file);
|
|
5196
|
-
if (!stat.isFile() || stat.isSymbolicLink() || stat.size !== entry.bytes)
|
|
5197
|
-
throw new Error("engine_pack_build_file_invalid");
|
|
5198
|
-
const bytes = readFileSync13(file);
|
|
5199
|
-
if (sha2562(bytes) !== entry.sha256)
|
|
5200
|
-
throw new Error("engine_pack_build_digest_mismatch");
|
|
5201
|
-
artifacts.set(path, path.endsWith(".json") ? bytes : Buffer.alloc(0));
|
|
5202
|
-
}
|
|
5203
|
-
const indexPath = safeRelativePath(runtime.packIndexUrl);
|
|
5204
|
-
const indexBytes = artifacts.get(indexPath);
|
|
5205
|
-
if (!indexBytes || indexBytes.length > 16 * 1024 * 1024)
|
|
5206
|
-
throw new Error("engine_pack_catalog_missing");
|
|
5207
|
-
const index = JSON.parse(indexBytes.toString("utf8"));
|
|
5208
|
-
if (!Array.isArray(index))
|
|
5209
|
-
throw new Error("engine_pack_catalog_invalid");
|
|
5210
|
-
const sources = new Set(sourcePaths);
|
|
5211
|
-
const rows = [];
|
|
5212
|
-
const seen = new Set;
|
|
5213
|
-
const inspect = [];
|
|
5214
|
-
for (const raw of index) {
|
|
5215
|
-
const row = object(raw);
|
|
5216
|
-
if (typeof row.sourcePath !== "string" || !sources.has(row.sourcePath))
|
|
5217
|
-
continue;
|
|
5218
|
-
if (typeof row.guid !== "string" || !UUID.test(row.guid) || seen.has(row.guid) || typeof row.kind !== "string" || typeof row.packageUrl !== "string" || !row.packageUrl.startsWith("/") || row.packageUrl.startsWith("//")) {
|
|
5219
|
-
throw new Error("engine_pack_catalog_invalid");
|
|
5220
|
-
}
|
|
5221
|
-
seen.add(row.guid);
|
|
5222
|
-
const packagePath = safeRelativePath(row.packageUrl.slice(1));
|
|
5223
|
-
const bytes = artifacts.get(packagePath);
|
|
5224
|
-
if (!bytes || bytes.length > 16 * 1024 * 1024)
|
|
5225
|
-
throw new Error("engine_pack_product_missing");
|
|
5226
|
-
const pack = object(JSON.parse(bytes.toString("utf8")));
|
|
5227
|
-
if (!Array.isArray(pack.assets))
|
|
5228
|
-
throw new Error("engine_pack_product_invalid");
|
|
5229
|
-
const matches = pack.assets.map(object).filter((asset) => asset.guid === row.guid && asset.kind === row.kind);
|
|
5230
|
-
if (matches.length !== 1)
|
|
5231
|
-
throw new Error("engine_pack_product_guid_missing");
|
|
5232
|
-
rows.push({
|
|
5233
|
-
guid: row.guid,
|
|
5234
|
-
kind: row.kind,
|
|
5235
|
-
source: row.sourcePath,
|
|
5236
|
-
sourcePath: row.sourcePath,
|
|
5237
|
-
...typeof row.name === "string" ? { name: row.name } : {},
|
|
5238
|
-
packageUrl: row.packageUrl
|
|
5239
|
-
});
|
|
5240
|
-
inspect.push({ guid: row.guid, kind: row.kind, packagePath, packageSha256: sha2562(bytes), verified: true });
|
|
5241
|
-
}
|
|
5242
|
-
if (!rows.some((row) => row.kind === "scene" || row.kind === "mesh"))
|
|
5243
|
-
throw new Error("engine_pack_readback_missing_renderable");
|
|
5244
|
-
return {
|
|
5245
|
-
authority: "engine-build-catalog",
|
|
5246
|
-
verify: { artifacts: artifacts.size, bytes: total, catalogSha256: sha2562(indexBytes) },
|
|
5247
|
-
list: rows,
|
|
5248
|
-
rows,
|
|
5249
|
-
inspect
|
|
5250
|
-
};
|
|
5251
|
-
}
|
|
5252
|
-
|
|
5253
|
-
// src/asset3d/transaction.ts
|
|
5254
|
-
function confined4(root, candidate) {
|
|
5255
|
-
const rel = relative10(root, candidate);
|
|
5256
|
-
return rel === "" || !isAbsolute5(rel) && rel !== ".." && !rel.startsWith(`..${sep7}`);
|
|
5257
|
-
}
|
|
5258
|
-
function forgeaxRoot(projectRoot) {
|
|
5259
|
-
return resolve12(projectRoot, ".forgeax");
|
|
5260
|
-
}
|
|
5261
|
-
function journalPath(projectRoot, execution) {
|
|
5262
|
-
return resolve12(forgeaxRoot(projectRoot), "asset3d-transactions", `${execution}.json`);
|
|
5263
|
-
}
|
|
5264
|
-
function providerResultPath(projectRoot, execution) {
|
|
5265
|
-
return resolve12(forgeaxRoot(projectRoot), "asset3d-results", `${execution}.json`);
|
|
5266
|
-
}
|
|
5267
|
-
function readInstall(projectRoot) {
|
|
5268
|
-
let manifest;
|
|
5269
|
-
try {
|
|
5270
|
-
manifest = JSON.parse(readFileSync14(resolve12(forgeaxRoot(projectRoot), "asset3d-install.json"), "utf8"));
|
|
5271
|
-
} catch {
|
|
5272
|
-
throw new Error("asset3d_not_installed");
|
|
5273
|
-
}
|
|
5274
|
-
if (manifest.schemaVersion !== INSTALL_SCHEMA || manifest.providerCommit.length !== 40)
|
|
5275
|
-
throw new Error("asset3d_install_manifest_invalid");
|
|
5276
|
-
return manifest;
|
|
5277
|
-
}
|
|
5278
|
-
function readJournal(projectRoot, execution) {
|
|
5279
|
-
if (!/^[0-9a-f-]{36}$/.test(execution))
|
|
5280
|
-
throw new Error("asset3d_execution_invalid");
|
|
5281
|
-
let journal;
|
|
5282
|
-
try {
|
|
5283
|
-
journal = JSON.parse(readFileSync14(journalPath(projectRoot, execution), "utf8"));
|
|
5284
|
-
} catch {
|
|
5285
|
-
throw new Error("asset3d_execution_not_found");
|
|
5286
|
-
}
|
|
5287
|
-
if (journal.schemaVersion !== TRANSACTION_SCHEMA || journal.execution !== execution)
|
|
5288
|
-
throw new Error("asset3d_journal_invalid");
|
|
5289
|
-
return journal;
|
|
5290
|
-
}
|
|
5291
|
-
function updateJournal(projectRoot, journal, patch) {
|
|
5292
|
-
const next = { ...journal, ...patch };
|
|
5293
|
-
atomicWrite(journalPath(projectRoot, journal.execution), `${JSON.stringify(next, null, 2)}
|
|
5294
|
-
`);
|
|
5295
|
-
return next;
|
|
5296
|
-
}
|
|
5297
|
-
function beginAsset3d(projectRootInput, queries, options = {}) {
|
|
5298
|
-
const projectRoot = realpathSync9(projectRootInput);
|
|
5299
|
-
if (queries.length < 1 || queries.length > 16 || queries.some((query) => [...query].length < 1 || [...query].length > 200)) {
|
|
5300
|
-
throw new Error("asset3d_queries_invalid: expected 1..16 queries of 1..200 characters");
|
|
5301
|
-
}
|
|
5302
|
-
const install = readInstall(projectRoot);
|
|
5303
|
-
const release = resolveEngineRelease(projectRoot, options.carrierPluginRoot === undefined ? {} : { pluginRoot: options.carrierPluginRoot });
|
|
5304
|
-
const execution = randomUUID4();
|
|
5305
|
-
const relativeOutput = `workspace/asset3d/${execution}`;
|
|
5306
|
-
const quarantine = resolve12(projectRoot, ".forgeax", "asset3d-quarantine");
|
|
5307
|
-
const allowedQuarantineRoot = resolve12(quarantine, relativeOutput);
|
|
5308
|
-
ensurePrivateDir(resolve12(projectRoot, ".forgeax", "asset3d-transactions"));
|
|
5309
|
-
ensurePrivateDir(allowedQuarantineRoot);
|
|
5310
|
-
const journal = {
|
|
5311
|
-
schemaVersion: TRANSACTION_SCHEMA,
|
|
5312
|
-
execution,
|
|
5313
|
-
queryDigest: sha2562(canonicalJson(queries)),
|
|
5314
|
-
providerCommit: install.providerCommit,
|
|
5315
|
-
engineVersion: release.version,
|
|
5316
|
-
engineCommit: release.commit,
|
|
5317
|
-
allowedQuarantineRoot,
|
|
5318
|
-
requestedCount: queries.length,
|
|
5319
|
-
createdAt: new Date().toISOString(),
|
|
5320
|
-
state: "begun"
|
|
5321
|
-
};
|
|
5322
|
-
atomicWrite(journalPath(projectRoot, execution), `${JSON.stringify(journal, null, 2)}
|
|
5323
|
-
`);
|
|
5324
|
-
return { execution, output_dir: relativeOutput };
|
|
5325
|
-
}
|
|
5326
|
-
function asset3dSearchOutputDir(projectRootInput, execution, queries) {
|
|
5327
|
-
const projectRoot = realpathSync9(projectRootInput);
|
|
5328
|
-
const journal = readJournal(projectRoot, execution);
|
|
5329
|
-
if (journal.state !== "begun") {
|
|
5330
|
-
throw new Error(`asset3d_execution_state_invalid: ${journal.state}`);
|
|
5331
|
-
}
|
|
5332
|
-
if (queries.length !== journal.requestedCount || sha2562(canonicalJson(queries)) !== journal.queryDigest) {
|
|
5333
|
-
throw new Error("asset3d_search_query_identity_mismatch");
|
|
5334
|
-
}
|
|
5335
|
-
const outputDir = `workspace/asset3d/${execution}`;
|
|
5336
|
-
const expectedRoot = resolve12(forgeaxRoot(projectRoot), "asset3d-quarantine", outputDir);
|
|
5337
|
-
if (journal.allowedQuarantineRoot !== expectedRoot || !existsSync9(expectedRoot) || realpathSync9(expectedRoot) !== expectedRoot) {
|
|
5338
|
-
throw new Error("asset3d_search_output_identity_mismatch");
|
|
5339
|
-
}
|
|
5340
|
-
return outputDir;
|
|
5341
|
-
}
|
|
5342
|
-
function recordAsset3dProviderResult(projectRootInput, execution, providerResult) {
|
|
5343
|
-
const projectRoot = realpathSync9(projectRootInput);
|
|
5344
|
-
const journal = readJournal(projectRoot, execution);
|
|
5345
|
-
if (journal.state !== "begun") {
|
|
5346
|
-
throw new Error(`asset3d_execution_state_invalid: ${journal.state}`);
|
|
5347
|
-
}
|
|
5348
|
-
const bytes = Buffer.byteLength(providerResult);
|
|
5349
|
-
if (bytes < 1 || bytes > MAX_JSON_BYTES) {
|
|
5350
|
-
throw new Error("provider_result_too_large: expected 1 byte..1 MiB");
|
|
5351
|
-
}
|
|
5352
|
-
ensurePrivateDir(resolve12(forgeaxRoot(projectRoot), "asset3d-results"));
|
|
5353
|
-
atomicWrite(providerResultPath(projectRoot, execution), providerResult, 384);
|
|
5354
|
-
updateJournal(projectRoot, journal, { state: "provider_complete" });
|
|
5355
|
-
}
|
|
5356
|
-
function fileBytesChecked(root, entry) {
|
|
5357
|
-
const source = resolve12(root, entry.path);
|
|
5358
|
-
if (!confined4(root, source))
|
|
5359
|
-
throw new Error("asset_manifest_escape");
|
|
5360
|
-
const canonicalRoot = realpathSync9(root);
|
|
5361
|
-
const canonicalSource = realpathSync9(source);
|
|
5362
|
-
if (!confined4(canonicalRoot, canonicalSource))
|
|
5363
|
-
throw new Error("asset_manifest_escape");
|
|
5364
|
-
const before = lstatSync9(source);
|
|
5365
|
-
if (!before.isFile() || before.isSymbolicLink())
|
|
5366
|
-
throw new Error("asset_manifest_not_regular");
|
|
5367
|
-
const fd = openSync4(source, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
5368
|
-
try {
|
|
5369
|
-
const opened = fstatSync(fd);
|
|
5370
|
-
if (!opened.isFile() || opened.size !== entry.bytes)
|
|
5371
|
-
throw new Error("asset_manifest_bytes_mismatch");
|
|
5372
|
-
const bytes = readFileSync14(fd);
|
|
5373
|
-
if (sha2562(bytes) !== entry.sha256)
|
|
5374
|
-
throw new Error("asset_manifest_digest_mismatch");
|
|
5375
|
-
return bytes;
|
|
5376
|
-
} finally {
|
|
5377
|
-
closeSync3(fd);
|
|
5378
|
-
}
|
|
5379
|
-
}
|
|
5380
|
-
function acquireLock2(projectRoot, assetId, timeoutMs) {
|
|
5381
|
-
const root = resolve12(forgeaxRoot(projectRoot), "asset3d-locks");
|
|
5382
|
-
ensurePrivateDir(root);
|
|
5383
|
-
const path = resolve12(root, `${assetId}.lock`);
|
|
5384
|
-
const token = randomUUID4();
|
|
5385
|
-
const deadline = Date.now() + timeoutMs;
|
|
5386
|
-
for (;; ) {
|
|
5387
|
-
try {
|
|
5388
|
-
const fd = openSync4(path, "wx", 384);
|
|
5389
|
-
writeFileSync10(fd, `${JSON.stringify({ token, pid: process.pid, acquiredAt: new Date().toISOString() })}
|
|
5390
|
-
`);
|
|
5391
|
-
closeSync3(fd);
|
|
5392
|
-
return () => {
|
|
5393
|
-
try {
|
|
5394
|
-
const current = JSON.parse(readFileSync14(path, "utf8"));
|
|
5395
|
-
if (current.token === token)
|
|
5396
|
-
unlinkSync3(path);
|
|
5397
|
-
} catch {}
|
|
5398
|
-
};
|
|
5399
|
-
} catch (error) {
|
|
5400
|
-
if (error.code !== "EEXIST")
|
|
5401
|
-
throw error;
|
|
5402
|
-
if (Date.now() >= deadline)
|
|
5403
|
-
throw new Error("asset_busy");
|
|
5404
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.min(100, deadline - Date.now()));
|
|
5405
|
-
}
|
|
5406
|
-
}
|
|
5407
|
-
}
|
|
5408
|
-
function engineCommand(projectRoot, args, carrierPluginRoot) {
|
|
5409
|
-
const release = resolveEngineRelease(projectRoot, carrierPluginRoot === undefined ? {} : { pluginRoot: carrierPluginRoot });
|
|
5410
|
-
const result = spawnSync4(process.execPath, [release.cliPath, ...args], {
|
|
5411
|
-
cwd: projectRoot,
|
|
5412
|
-
encoding: "utf8",
|
|
5413
|
-
maxBuffer: 1024 * 1024 + 1,
|
|
5414
|
-
timeout: 150000
|
|
5415
|
-
});
|
|
5416
|
-
const stdout = result.stdout ?? "";
|
|
5417
|
-
if (Buffer.byteLength(stdout) > 1024 * 1024)
|
|
5418
|
-
throw new Error("engine_envelope_too_large");
|
|
5419
|
-
const lines = stdout.split(`
|
|
5420
|
-
`).filter((line) => line.length > 0);
|
|
5421
|
-
if (lines.length !== 1 || !stdout.endsWith(`
|
|
5422
|
-
`))
|
|
5423
|
-
throw new Error("engine_terminal_envelope_invalid");
|
|
5424
|
-
let envelope;
|
|
5425
|
-
try {
|
|
5426
|
-
envelope = JSON.parse(lines[0]);
|
|
5427
|
-
} catch {
|
|
5428
|
-
throw new Error("engine_terminal_envelope_invalid");
|
|
5429
|
-
}
|
|
5430
|
-
if (envelope.schemaVersion !== "1.0.0" || typeof envelope.command !== "string" || typeof envelope.ok !== "boolean") {
|
|
5431
|
-
throw new Error("engine_terminal_envelope_invalid");
|
|
5432
|
-
}
|
|
5433
|
-
if (result.status !== 0 || envelope.ok !== true) {
|
|
5434
|
-
throw new Error(`engine_${String(envelope.command).replace(".", "_")}_failed:${canonicalJson(envelope.error ?? { exitCode: result.status })}`);
|
|
5435
|
-
}
|
|
5436
|
-
return envelope;
|
|
5437
|
-
}
|
|
5438
|
-
function engineAddedRows(projectRoot, asset) {
|
|
5439
|
-
let subAssets = asset.subAssets;
|
|
5440
|
-
if (!Array.isArray(subAssets)) {
|
|
5441
|
-
if (typeof asset.metaPath !== "string")
|
|
5442
|
-
throw new Error("engine_readback_missing_meta");
|
|
5443
|
-
const metaPath = resolve12(projectRoot, asset.metaPath);
|
|
5444
|
-
if (!confined4(projectRoot, metaPath))
|
|
5445
|
-
throw new Error("engine_readback_meta_escape");
|
|
5446
|
-
const canonical = realpathSync9(metaPath);
|
|
5447
|
-
const info = lstatSync9(metaPath);
|
|
5448
|
-
if (!confined4(projectRoot, canonical) || !info.isFile() || info.isSymbolicLink()) {
|
|
5449
|
-
throw new Error("engine_readback_meta_invalid");
|
|
5450
|
-
}
|
|
5451
|
-
let meta;
|
|
5452
|
-
try {
|
|
5453
|
-
meta = JSON.parse(readFileSync14(metaPath, "utf8"));
|
|
5454
|
-
} catch {
|
|
5455
|
-
throw new Error("engine_readback_meta_invalid");
|
|
5456
|
-
}
|
|
5457
|
-
if (!Array.isArray(meta.subAssets))
|
|
5458
|
-
throw new Error("engine_readback_missing_subassets");
|
|
5459
|
-
subAssets = meta.subAssets.filter((row) => row !== null && typeof row === "object" && !Array.isArray(row));
|
|
5460
|
-
}
|
|
5461
|
-
const projectPath = (value) => {
|
|
5462
|
-
if (typeof value !== "string")
|
|
5463
|
-
return value;
|
|
5464
|
-
const absolute = resolve12(projectRoot, value);
|
|
5465
|
-
if (!confined4(projectRoot, absolute))
|
|
5466
|
-
throw new Error("engine_readback_path_escape");
|
|
5467
|
-
return relative10(projectRoot, absolute).split(sep7).join("/");
|
|
5468
|
-
};
|
|
5469
|
-
return subAssets.map((row) => ({ source: projectPath(asset.source), metaPath: projectPath(asset.metaPath), reused: asset.reused, ...row }));
|
|
5470
|
-
}
|
|
5471
|
-
function engineReadback(projectRoot, assetRelative, add, carrierPluginRoot) {
|
|
5472
|
-
const verify = engineCommand(projectRoot, ["asset", "verify", "--json"], carrierPluginRoot);
|
|
5473
|
-
const list = engineCommand(projectRoot, ["asset", "list", "--json"], carrierPluginRoot);
|
|
5474
|
-
const addAssets = add?.value?.assets ?? [];
|
|
5475
|
-
const rows = addAssets.flatMap((asset) => engineAddedRows(projectRoot, asset));
|
|
5476
|
-
const guids = [...new Set(rows.flatMap((row) => typeof row.guid === "string" ? [row.guid] : []))];
|
|
5477
|
-
if (add && guids.length === 0)
|
|
5478
|
-
throw new Error("engine_readback_missing_guid");
|
|
5479
|
-
const inspect = guids.map((guid) => engineCommand(projectRoot, ["asset", "inspect", guid, "--json"], carrierPluginRoot).value);
|
|
5480
|
-
const listed = Array.isArray(list.value) ? list.value : Array.isArray(list.value?.assets) ? list.value.assets : [];
|
|
5481
|
-
for (const guid of guids)
|
|
5482
|
-
if (!listed.some((entry) => entry.guid === guid))
|
|
5483
|
-
throw new Error("engine_catalog_readback_missing");
|
|
5484
|
-
return { ...add ? { add: add.value } : {}, verify: verify.value, list: listed, inspect, rows };
|
|
5485
|
-
}
|
|
5486
|
-
function packReadback(projectRoot, item, carrierPluginRoot) {
|
|
5487
|
-
const release = resolveEngineRelease(projectRoot, carrierPluginRoot === undefined ? {} : { pluginRoot: carrierPluginRoot });
|
|
5488
|
-
const result = spawnSync4(process.execPath, [release.cliPath, "build", "--json"], {
|
|
5489
|
-
cwd: projectRoot,
|
|
5490
|
-
encoding: "utf8",
|
|
5491
|
-
timeout: 150000,
|
|
5492
|
-
maxBuffer: 8 * 1024 * 1024
|
|
5493
|
-
});
|
|
5494
|
-
if (result.error || result.status !== 0)
|
|
5495
|
-
throw new Error("engine_pack_build_failed");
|
|
5496
|
-
const envelope = parseEnvelope(result.stdout, "build");
|
|
5497
|
-
const prefix = `assets/3d/ea-3d/${item.providerAssetId}/`;
|
|
5498
|
-
return readPackBuildCatalog(projectRoot, item.manifest.map((entry) => prefix + entry.path), envelope.value);
|
|
5499
|
-
}
|
|
5500
|
-
function priorProvenance(destination) {
|
|
5501
|
-
try {
|
|
5502
|
-
return JSON.parse(readFileSync14(resolve12(destination, ".forgeax-asset.json"), "utf8"));
|
|
5503
|
-
} catch {
|
|
5504
|
-
return;
|
|
5505
|
-
}
|
|
5506
|
-
}
|
|
5507
|
-
function liveFilesMatch(destination, item) {
|
|
5508
|
-
return item.manifest.every((entry) => {
|
|
5509
|
-
const path = resolve12(destination, entry.path);
|
|
5510
|
-
try {
|
|
5511
|
-
return confined4(destination, path) && statSync6(path).isFile() && statSync6(path).size === entry.bytes && sha2562(readFileSync14(path)) === entry.sha256;
|
|
5512
|
-
} catch {
|
|
5513
|
-
return false;
|
|
5514
|
-
}
|
|
5515
|
-
});
|
|
5516
|
-
}
|
|
5517
|
-
function quarantineFiles(root, directory = root) {
|
|
5518
|
-
const files = [];
|
|
5519
|
-
for (const name of readdirSync7(directory)) {
|
|
5520
|
-
const path = resolve12(directory, name);
|
|
5521
|
-
const info = lstatSync9(path);
|
|
5522
|
-
if (info.isSymbolicLink())
|
|
5523
|
-
throw new Error("asset_quarantine_symlink_rejected");
|
|
5524
|
-
if (info.isDirectory())
|
|
5525
|
-
files.push(...quarantineFiles(root, path));
|
|
5526
|
-
else if (info.isFile())
|
|
5527
|
-
files.push(relative10(root, path).split(sep7).join("/"));
|
|
5528
|
-
else
|
|
5529
|
-
throw new Error("asset_quarantine_special_file_rejected");
|
|
5530
|
-
}
|
|
5531
|
-
return files;
|
|
5532
|
-
}
|
|
5533
|
-
function validateQuarantine(journal, items) {
|
|
5534
|
-
const root = realpathSync9(journal.allowedQuarantineRoot);
|
|
5535
|
-
const expected = items.flatMap((item) => item.manifest.map((entry) => entry.path)).sort();
|
|
5536
|
-
if (new Set(expected).size !== expected.length)
|
|
5537
|
-
throw new Error("provider_result_invalid: cross-item manifest collision");
|
|
5538
|
-
const actual = quarantineFiles(root).sort();
|
|
5539
|
-
if (canonicalJson(actual) !== canonicalJson(expected))
|
|
5540
|
-
throw new Error("asset_quarantine_undeclared_file");
|
|
5541
|
-
}
|
|
5542
|
-
function stageItem(projectRoot, journal, item, destination, prior) {
|
|
5543
|
-
const root = realpathSync9(journal.allowedQuarantineRoot);
|
|
5544
|
-
const stageRoot = resolve12(forgeaxRoot(projectRoot), "asset3d-staging", journal.execution, item.providerAssetId);
|
|
5545
|
-
rmSync6(stageRoot, { recursive: true, force: true });
|
|
5546
|
-
ensurePrivateDir(resolve12(stageRoot, ".."));
|
|
5547
|
-
if (existsSync9(destination))
|
|
5548
|
-
cpSync2(destination, stageRoot, { recursive: true, errorOnExist: true, force: false });
|
|
5549
|
-
else
|
|
5550
|
-
ensurePrivateDir(stageRoot);
|
|
5551
|
-
const nextPaths = new Set(item.manifest.map((entry) => entry.path));
|
|
5552
|
-
const previousFiles = Array.isArray(prior?.files) ? prior.files : [];
|
|
5553
|
-
for (const previous of previousFiles) {
|
|
5554
|
-
if (typeof previous.relativePath !== "string" || nextPaths.has(previous.relativePath))
|
|
5555
|
-
continue;
|
|
5556
|
-
const stale = resolve12(stageRoot, previous.relativePath);
|
|
5557
|
-
if (!confined4(stageRoot, stale))
|
|
5558
|
-
throw new Error("asset_provenance_path_escape");
|
|
5559
|
-
rmSync6(stale, { force: true });
|
|
5560
|
-
rmSync6(`${stale}.meta.json`, { force: true });
|
|
5561
|
-
}
|
|
5562
|
-
for (const entry of item.manifest) {
|
|
5563
|
-
const target = resolve12(stageRoot, entry.path);
|
|
5564
|
-
if (!confined4(stageRoot, target))
|
|
5565
|
-
throw new Error("asset_manifest_escape");
|
|
5566
|
-
mkdirSync10(resolve12(target, ".."), { recursive: true, mode: 448 });
|
|
5567
|
-
atomicWrite(target, fileBytesChecked(root, entry), 384);
|
|
5568
|
-
}
|
|
5569
|
-
return stageRoot;
|
|
5570
|
-
}
|
|
5571
|
-
function provenance(item, install, readback, refreshed) {
|
|
5572
|
-
const rows = readback.rows;
|
|
5573
|
-
const sourcePrefix = `assets/3d/ea-3d/${item.providerAssetId}/`;
|
|
5574
|
-
return {
|
|
5575
|
-
schemaVersion: PROVENANCE_SCHEMA,
|
|
5576
|
-
provider: "ea-3d",
|
|
5577
|
-
providerAssetId: item.providerAssetId,
|
|
5578
|
-
providerCommit: install.providerCommit,
|
|
5579
|
-
originSetDigest: install.originSetDigest,
|
|
5580
|
-
aggregateSha256: item.sha256,
|
|
5581
|
-
bytes: item.bytes,
|
|
5582
|
-
deliveredFormat: item.deliveredFormat,
|
|
5583
|
-
...item.deliveredFormat === "pack" ? { primaryPack: item.primaryPack } : { primaryModel: item.primaryModel },
|
|
5584
|
-
engine: { version: ENGINE_VERSION, commit: ENGINE_COMMIT },
|
|
5585
|
-
refreshed,
|
|
5586
|
-
files: item.manifest.map((entry) => ({
|
|
5587
|
-
relativePath: entry.path,
|
|
5588
|
-
role: entry.role,
|
|
5589
|
-
sha256: entry.sha256,
|
|
5590
|
-
bytes: entry.bytes,
|
|
5591
|
-
engineRows: rows.filter((row) => row.source === `${sourcePrefix}${entry.path}`)
|
|
5592
|
-
})),
|
|
5593
|
-
catalog: { verify: readback.verify, list: readback.list, inspect: readback.inspect }
|
|
5594
|
-
};
|
|
5595
|
-
}
|
|
5596
|
-
function commitItem(projectRoot, initialJournal, install, item, refresh, lockTimeoutMs, carrierPluginRoot) {
|
|
5597
|
-
const destination = resolve12(projectRoot, "assets", "3d", "ea-3d", item.providerAssetId);
|
|
5598
|
-
const assetRoot = resolve12(projectRoot, "assets", "3d", "ea-3d");
|
|
5599
|
-
ensurePrivateDir(assetRoot);
|
|
5600
|
-
if (!confined4(assetRoot, destination))
|
|
5601
|
-
throw new Error("asset_destination_escape");
|
|
5602
|
-
const releaseLock = acquireLock2(projectRoot, item.providerAssetId, lockTimeoutMs);
|
|
5603
|
-
let journal = initialJournal;
|
|
5604
|
-
try {
|
|
5605
|
-
const prior = priorProvenance(destination);
|
|
5606
|
-
const priorDigest = typeof prior?.aggregateSha256 === "string" ? prior.aggregateSha256 : undefined;
|
|
5607
|
-
if (priorDigest === item.sha256 && liveFilesMatch(destination, item)) {
|
|
5608
|
-
const priorRows = Array.isArray(prior?.files) ? prior.files.flatMap((entry) => entry.engineRows ?? []) : [];
|
|
5609
|
-
const baseReadback = item.deliveredFormat === "pack" ? packReadback(projectRoot, item, carrierPluginRoot) : engineReadback(projectRoot, `assets/3d/ea-3d/${item.providerAssetId}`, undefined, carrierPluginRoot);
|
|
5610
|
-
const guids = priorRows.flatMap((row) => typeof row.guid === "string" ? [row.guid] : []);
|
|
5611
|
-
const listed = Array.isArray(baseReadback.list) ? baseReadback.list : [];
|
|
5612
|
-
if (guids.length === 0 || guids.some((guid) => !listed.some((entry) => entry.guid === guid)))
|
|
5613
|
-
throw new Error("asset_reuse_readback_failed");
|
|
5614
|
-
const readback = item.deliveredFormat === "pack" ? baseReadback : { ...baseReadback, inspect: guids.map((guid) => engineCommand(projectRoot, ["asset", "inspect", guid, "--json"], carrierPluginRoot).value) };
|
|
5615
|
-
journal = updateJournal(projectRoot, journal, { state: "committed", previousDigest: priorDigest, newDigest: item.sha256 });
|
|
5616
|
-
return { providerAssetId: item.providerAssetId, digest: item.sha256, bytes: item.bytes, reused: true, refreshed: false, sourcePath: relative10(projectRoot, destination), provenancePath: relative10(projectRoot, resolve12(destination, ".forgeax-asset.json")), engine: { version: ENGINE_VERSION, commit: ENGINE_COMMIT }, catalog: readback, rows: priorRows };
|
|
5617
|
-
}
|
|
5618
|
-
if (priorDigest && priorDigest !== item.sha256 && !refresh)
|
|
5619
|
-
throw new Error("asset_changed");
|
|
5620
|
-
const stage = stageItem(projectRoot, journal, item, destination, prior);
|
|
5621
|
-
const backup = resolve12(forgeaxRoot(projectRoot), "asset3d-backups", journal.execution, item.providerAssetId);
|
|
5622
|
-
ensurePrivateDir(resolve12(backup, ".."));
|
|
5623
|
-
journal = updateJournal(projectRoot, journal, { state: "committing", destination, backupPath: existsSync9(destination) ? backup : undefined, previousDigest: priorDigest, newDigest: item.sha256 });
|
|
5624
|
-
if (existsSync9(destination))
|
|
5625
|
-
renameSync7(destination, backup);
|
|
5626
|
-
renameSync7(stage, destination);
|
|
5627
|
-
try {
|
|
5628
|
-
const assetRelative = `assets/3d/ea-3d/${item.providerAssetId}`;
|
|
5629
|
-
const readback = item.deliveredFormat === "pack" ? packReadback(projectRoot, item, carrierPluginRoot) : engineReadback(projectRoot, assetRelative, engineCommand(projectRoot, ["asset", "add", assetRelative, "--reimport-policy", "semantic-only", "--json"], carrierPluginRoot), carrierPluginRoot);
|
|
5630
|
-
if (item.deliveredFormat === "pack" && !liveFilesMatch(destination, item))
|
|
5631
|
-
throw new Error("engine_pack_source_changed");
|
|
5632
|
-
atomicWrite(resolve12(destination, ".forgeax-asset.json"), `${JSON.stringify(provenance(item, install, readback, priorDigest !== undefined), null, 2)}
|
|
5633
|
-
`);
|
|
5634
|
-
journal = updateJournal(projectRoot, journal, { state: "engine_verified" });
|
|
5635
|
-
if (existsSync9(backup))
|
|
5636
|
-
rmSync6(backup, { recursive: true, force: true });
|
|
5637
|
-
journal = updateJournal(projectRoot, journal, { state: "committed" });
|
|
5638
|
-
return { providerAssetId: item.providerAssetId, digest: item.sha256, bytes: item.bytes, reused: false, refreshed: priorDigest !== undefined, sourcePath: assetRelative, provenancePath: `${assetRelative}/.forgeax-asset.json`, engine: { version: ENGINE_VERSION, commit: ENGINE_COMMIT }, catalog: readback, rows: readback.rows };
|
|
5639
|
-
} catch (error) {
|
|
5640
|
-
rmSync6(destination, { recursive: true, force: true });
|
|
5641
|
-
if (existsSync9(backup))
|
|
5642
|
-
renameSync7(backup, destination);
|
|
5643
|
-
updateJournal(projectRoot, journal, { state: "rolled_back", error: error instanceof Error ? error.message.slice(0, 256) : "engine failure" });
|
|
5644
|
-
throw error;
|
|
5645
|
-
}
|
|
5646
|
-
} finally {
|
|
5647
|
-
releaseLock();
|
|
5648
|
-
}
|
|
5649
|
-
}
|
|
5650
|
-
function commitAsset3d(options) {
|
|
5651
|
-
const projectRoot = realpathSync9(options.projectRoot);
|
|
5652
|
-
const install = readInstall(projectRoot);
|
|
5653
|
-
let journal = readJournal(projectRoot, options.execution);
|
|
5654
|
-
if (journal.state !== "begun" && journal.state !== "provider_complete" && journal.state !== "validated") {
|
|
5655
|
-
throw new Error(`asset3d_execution_state_invalid: ${journal.state}`);
|
|
5656
|
-
}
|
|
5657
|
-
if (journal.providerCommit !== install.providerCommit || journal.engineCommit !== ENGINE_COMMIT || journal.engineVersion !== ENGINE_VERSION) {
|
|
5658
|
-
throw new Error("asset3d_execution_identity_mismatch");
|
|
5659
|
-
}
|
|
5660
|
-
let providerResult = options.providerResult;
|
|
5661
|
-
if (providerResult === undefined) {
|
|
5662
|
-
try {
|
|
5663
|
-
providerResult = readFileSync14(providerResultPath(projectRoot, options.execution));
|
|
5664
|
-
} catch {
|
|
5665
|
-
throw new Error("asset3d_provider_result_missing: call search_asset for this execution first");
|
|
5666
|
-
}
|
|
5667
|
-
}
|
|
5668
|
-
let result;
|
|
5669
|
-
try {
|
|
5670
|
-
result = parseProviderResult(providerResult, install.providerCommit, install.originSetDigest);
|
|
5671
|
-
if (result.total !== journal.requestedCount)
|
|
5672
|
-
throw new Error("provider_result_invalid: requested count mismatch");
|
|
5673
|
-
const orderedQueries = [...result.results].sort((left, right) => left.queryIndex - right.queryIndex).map((item) => item.query);
|
|
5674
|
-
if (sha2562(canonicalJson(orderedQueries)) !== journal.queryDigest) {
|
|
5675
|
-
throw new Error("provider_result_invalid: query identity mismatch");
|
|
5676
|
-
}
|
|
5677
|
-
const successes = result.results.filter((item) => item.status === "ok");
|
|
5678
|
-
validateQuarantine(journal, successes);
|
|
5679
|
-
journal = updateJournal(projectRoot, journal, { state: "validated" });
|
|
5680
|
-
} catch (error) {
|
|
5681
|
-
rmSync6(journal.allowedQuarantineRoot, { recursive: true, force: true });
|
|
5682
|
-
rmSync6(providerResultPath(projectRoot, options.execution), { force: true });
|
|
5683
|
-
updateJournal(projectRoot, journal, {
|
|
5684
|
-
state: "failed",
|
|
5685
|
-
error: error instanceof Error ? error.message.slice(0, 256) : "provider validation failed"
|
|
5686
|
-
});
|
|
5687
|
-
throw error;
|
|
5688
|
-
}
|
|
5689
|
-
const terminal = [];
|
|
5690
|
-
for (const item of result.results) {
|
|
5691
|
-
if (item.status === "error") {
|
|
5692
|
-
terminal.push(item);
|
|
5693
|
-
continue;
|
|
5694
|
-
}
|
|
5695
|
-
try {
|
|
5696
|
-
terminal.push({ status: "ok", ...commitItem(projectRoot, journal, install, item, options.refresh === true, options.lockTimeoutMs ?? 30000, options.carrierPluginRoot) });
|
|
5697
|
-
} catch (error) {
|
|
5698
|
-
terminal.push({ status: "error", providerAssetId: item.providerAssetId, code: error instanceof Error ? error.message.split(":", 1)[0] : "internal_error", retryable: false });
|
|
5699
|
-
}
|
|
5700
|
-
}
|
|
5701
|
-
rmSync6(journal.allowedQuarantineRoot, { recursive: true, force: true });
|
|
5702
|
-
rmSync6(providerResultPath(projectRoot, options.execution), { force: true });
|
|
5703
|
-
const failed = terminal.filter((entry) => entry.status === "error").length;
|
|
5704
|
-
updateJournal(projectRoot, journal, { state: failed === 0 ? "complete" : "failed" });
|
|
5705
|
-
return {
|
|
5706
|
-
schemaVersion: "forgeax.asset3d-commit-result/1.0.0",
|
|
5707
|
-
execution: journal.execution,
|
|
5708
|
-
succeeded: terminal.filter((entry) => entry.status === "ok").length,
|
|
5709
|
-
failed,
|
|
5710
|
-
results: terminal
|
|
5711
|
-
};
|
|
5712
|
-
}
|
|
5713
|
-
function abortAsset3d(projectRootInput, execution) {
|
|
5714
|
-
const projectRoot = realpathSync9(projectRootInput);
|
|
5715
|
-
const journal = readJournal(projectRoot, execution);
|
|
5716
|
-
if (journal.state === "committed" || journal.state === "complete" || journal.state === "engine_verified") {
|
|
5717
|
-
throw new Error("asset3d_execution_already_committed");
|
|
5718
|
-
}
|
|
5719
|
-
rmSync6(journal.allowedQuarantineRoot, { recursive: true, force: true });
|
|
5720
|
-
rmSync6(providerResultPath(projectRoot, execution), { force: true });
|
|
5721
|
-
updateJournal(projectRoot, journal, { state: "aborted" });
|
|
5722
|
-
return { execution, aborted: true };
|
|
5723
|
-
}
|
|
5724
|
-
function doctorAsset3d(projectRootInput, options = {}) {
|
|
5725
|
-
const projectRoot = realpathSync9(projectRootInput);
|
|
5726
|
-
const install = readInstall(projectRoot);
|
|
5727
|
-
const release = resolveEngineRelease(projectRoot, options.carrierPluginRoot === undefined ? {} : { pluginRoot: options.carrierPluginRoot });
|
|
5728
|
-
const recovered = [];
|
|
5729
|
-
const root = resolve12(forgeaxRoot(projectRoot), "asset3d-transactions");
|
|
5730
|
-
if (existsSync9(root))
|
|
5731
|
-
for (const file of readdirSync7(root).filter((name) => name.endsWith(".json"))) {
|
|
5732
|
-
try {
|
|
5733
|
-
const path = resolve12(root, file);
|
|
5734
|
-
const journal = JSON.parse(readFileSync14(path, "utf8"));
|
|
5735
|
-
if (journal.schemaVersion !== TRANSACTION_SCHEMA || journal.state !== "committing" || !journal.destination)
|
|
5736
|
-
continue;
|
|
5737
|
-
const assetsRoot = resolve12(projectRoot, "assets", "3d", "ea-3d");
|
|
5738
|
-
const backupsRoot = resolve12(forgeaxRoot(projectRoot), "asset3d-backups");
|
|
5739
|
-
if (!confined4(assetsRoot, journal.destination) || journal.backupPath && !confined4(backupsRoot, journal.backupPath))
|
|
5740
|
-
continue;
|
|
5741
|
-
rmSync6(journal.destination, { recursive: true, force: true });
|
|
5742
|
-
if (journal.backupPath && existsSync9(journal.backupPath))
|
|
5743
|
-
renameSync7(journal.backupPath, journal.destination);
|
|
5744
|
-
updateJournal(projectRoot, journal, { state: "rolled_back", error: "stale_committing_recovered" });
|
|
5745
|
-
recovered.push(journal.execution);
|
|
5746
|
-
} catch {}
|
|
5747
|
-
}
|
|
5748
|
-
return { installed: true, recovered, engine: { version: release.version, commit: release.commit }, provider: { commit: install.providerCommit, cache: install.providerCache, live: existsSync9(resolve12(install.providerCache, "venv", "bin", "python")) } };
|
|
5749
|
-
}
|
|
5750
|
-
|
|
5751
|
-
// src/asset3d/aw-access.ts
|
|
5752
|
-
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
5753
|
-
import { existsSync as existsSync10 } from "node:fs";
|
|
5754
|
-
import { resolve as resolve13 } from "node:path";
|
|
5755
|
-
var AW_SERVICE_PATH = "/trpc.oasismetric.omcontentserver.http";
|
|
5756
|
-
var ACCESS_CHECK_SCHEMA = "forgeax.asset3d-access-check/1.0.0";
|
|
5757
|
-
var DEFAULT_AW_PUBLIC_SERVICE_ROOT = "http://lb-pl74wsqg-5wi8ujmy1fq2746r.clb.usw-tencentclb.com:8008/trpc.oasismetric.omcontentserver.http";
|
|
5758
|
-
var DEFAULT_EA_LOCAL_SERVICE_ROOT = "http://test-ultrongw.woa.com/trpc.oasismetric.omcontentserver.http";
|
|
5759
|
-
function normalizeAssetLibraryServiceRoot(input) {
|
|
5760
|
-
let parsed;
|
|
5761
|
-
try {
|
|
5762
|
-
parsed = new URL(input);
|
|
5763
|
-
} catch {
|
|
5764
|
-
throw new Error("asset3d_base_url_invalid: expected an HTTP(S) EA gateway or service URL");
|
|
5765
|
-
}
|
|
5766
|
-
if (!["http:", "https:"].includes(parsed.protocol) || parsed.username || parsed.password || parsed.search || parsed.hash) {
|
|
5767
|
-
throw new Error("asset3d_base_url_invalid: credentials, query, and fragment are forbidden");
|
|
5768
|
-
}
|
|
5769
|
-
let path = parsed.pathname.replace(/\/+$/, "");
|
|
5770
|
-
if (path.endsWith(`${AW_SERVICE_PATH}/HybridSearch`))
|
|
5771
|
-
path = path.slice(0, -"/HybridSearch".length);
|
|
5772
|
-
else if (!path.endsWith(AW_SERVICE_PATH))
|
|
5773
|
-
path = `${path}${AW_SERVICE_PATH}`;
|
|
5774
|
-
parsed.pathname = path;
|
|
5775
|
-
return parsed.toString().replace(/\/$/, "");
|
|
5776
|
-
}
|
|
5777
|
-
function resolveAssetLibrarySelection(options = {}) {
|
|
5778
|
-
const library = options.library || process.env.FORGEAX_ASSET_LIBRARY || "aw";
|
|
5779
|
-
if (library !== "aw" && library !== "ea") {
|
|
5780
|
-
throw new Error("asset3d_library_invalid: expected aw or ea");
|
|
5781
|
-
}
|
|
5782
|
-
const defaultRoot = library === "ea" ? DEFAULT_EA_LOCAL_SERVICE_ROOT : DEFAULT_AW_PUBLIC_SERVICE_ROOT;
|
|
5783
|
-
const configured = options.baseUrl || process.env.FORGEAX_ASSET_LIBRARY_BASE_URL || defaultRoot;
|
|
5784
|
-
return { library, serviceRoot: normalizeAssetLibraryServiceRoot(configured) };
|
|
5785
|
-
}
|
|
5786
|
-
function checkAssetLibraryProviderAccess(options) {
|
|
5787
|
-
const command = resolve13(options.providerCache, "bin", "asset3d-search");
|
|
5788
|
-
if (!existsSync10(command))
|
|
5789
|
-
throw new Error("asset3d_provider_not_prepared");
|
|
5790
|
-
const result = spawnSync5(command, ["--check-aw-access"], {
|
|
5791
|
-
encoding: "utf8",
|
|
5792
|
-
timeout: 45000,
|
|
5793
|
-
maxBuffer: 128 * 1024,
|
|
5794
|
-
env: {
|
|
5795
|
-
...process.env,
|
|
5796
|
-
ASSET3D_CATALOG_BASE_URL: "",
|
|
5797
|
-
AW_API_BASE_URL: options.serviceRoot,
|
|
5798
|
-
AW_API_DEPOT_NAME: options.depotName,
|
|
5799
|
-
AW_API_CREDENTIAL_FILE: resolve13(options.credentialFile),
|
|
5800
|
-
AW_API_SANDBOX_KEY: ""
|
|
5801
|
-
}
|
|
5802
|
-
});
|
|
5803
|
-
let payload;
|
|
5804
|
-
try {
|
|
5805
|
-
payload = JSON.parse(result.stdout);
|
|
5806
|
-
} catch {
|
|
5807
|
-
throw new Error("asset3d_access_validation_failed: Provider returned an invalid response");
|
|
5808
|
-
}
|
|
5809
|
-
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
5810
|
-
throw new Error("asset3d_access_validation_failed: Provider returned an invalid response");
|
|
5811
|
-
}
|
|
5812
|
-
const envelope = payload;
|
|
5813
|
-
if (result.status !== 0 || envelope.ok !== true) {
|
|
5814
|
-
const code = envelope.error?.code === "asset3d_access_validation_inconclusive" ? "asset3d_access_validation_inconclusive" : "asset3d_api_key_invalid_or_unavailable";
|
|
5815
|
-
throw new Error(`${code}: verify the selected asset-library service, network access, and Sandbox Key`);
|
|
5816
|
-
}
|
|
5817
|
-
if (envelope.schemaVersion !== ACCESS_CHECK_SCHEMA || envelope.value?.authentication !== "sandbox-key" || !Array.isArray(envelope.value.downloadOrigins)) {
|
|
5818
|
-
throw new Error("asset3d_access_validation_failed: Provider returned an invalid response");
|
|
5819
|
-
}
|
|
5820
|
-
const origins = canonicalizeOrigins(envelope.value.downloadOrigins).values;
|
|
5821
|
-
return { serviceRoot: options.serviceRoot, authentication: "sandbox-key", downloadOrigins: origins };
|
|
5822
|
-
}
|
|
5823
|
-
|
|
5824
|
-
// src/asset3d/credentials.ts
|
|
5825
|
-
import { chmodSync as chmodSync5, existsSync as existsSync11, lstatSync as lstatSync10, readFileSync as readFileSync15, statSync as statSync7, unlinkSync as unlinkSync4 } from "node:fs";
|
|
5826
|
-
import { homedir as homedir4 } from "node:os";
|
|
5827
|
-
import { dirname as dirname11, isAbsolute as isAbsolute6, resolve as resolve14 } from "node:path";
|
|
5828
|
-
var AW_CREDENTIAL_SCHEMA = "forgeax.asset3d-credential/1.0.0";
|
|
5829
|
-
var AW_KEY_ENV = "FORGEAX_ASSET3D_AW_SANDBOX_KEY";
|
|
5830
|
-
var MAX_CREDENTIAL_BYTES = 4096;
|
|
5831
|
-
function validateKey(value) {
|
|
5832
|
-
if (!value || value.length > 2048 || [...value].some((character) => {
|
|
5833
|
-
const code = character.charCodeAt(0);
|
|
5834
|
-
return code < 32 || code === 127;
|
|
5835
|
-
})) {
|
|
5836
|
-
throw new Error("asset3d_api_key_invalid: expected a non-empty printable key");
|
|
5837
|
-
}
|
|
5838
|
-
return value;
|
|
5839
|
-
}
|
|
5840
|
-
function defaultAwCredentialFile() {
|
|
5841
|
-
const configured = process.env.FORGEAX_ASSET3D_CREDENTIAL_FILE;
|
|
5842
|
-
return resolve14(configured || resolve14(homedir4(), ".forgeax", "credentials", "asset3d-aw.json"));
|
|
5843
|
-
}
|
|
5844
|
-
function readAwCredential(pathInput) {
|
|
5845
|
-
const path = resolve14(pathInput);
|
|
5846
|
-
if (!isAbsolute6(pathInput))
|
|
5847
|
-
throw new Error("asset3d_credential_path_invalid: absolute path required");
|
|
5848
|
-
if (!existsSync11(path))
|
|
5849
|
-
return;
|
|
5850
|
-
try {
|
|
5851
|
-
const metadata = lstatSync10(path);
|
|
5852
|
-
const wrongOwner = typeof process.getuid === "function" && metadata.uid !== process.getuid();
|
|
5853
|
-
if (!metadata.isFile() || metadata.isSymbolicLink() || wrongOwner)
|
|
5854
|
-
throw new Error;
|
|
5855
|
-
if ((metadata.mode & 63) !== 0 || metadata.size < 1 || metadata.size > MAX_CREDENTIAL_BYTES)
|
|
5856
|
-
throw new Error;
|
|
5857
|
-
const parsed = JSON.parse(readFileSync15(path, "utf8"));
|
|
5858
|
-
if (Object.keys(parsed).sort().join(",") !== "provider,sandboxKey,schemaVersion")
|
|
5859
|
-
throw new Error;
|
|
5860
|
-
if (parsed.schemaVersion !== AW_CREDENTIAL_SCHEMA || parsed.provider !== "aw" || typeof parsed.sandboxKey !== "string")
|
|
5861
|
-
throw new Error;
|
|
5862
|
-
return validateKey(parsed.sandboxKey);
|
|
5863
|
-
} catch {
|
|
5864
|
-
throw new Error("asset3d_credential_invalid: credential file must be an owned 0600 regular file with the supported schema");
|
|
5865
|
-
}
|
|
5866
|
-
}
|
|
5867
|
-
async function promptAwKey() {
|
|
5868
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY || typeof process.stdin.setRawMode !== "function") {
|
|
5869
|
-
throw new Error(`asset3d_api_key_required: set ${AW_KEY_ENV} or rerun in an interactive terminal`);
|
|
5870
|
-
}
|
|
5871
|
-
process.stdout.write("AW Asset3D Sandbox Key (input hidden): ");
|
|
5872
|
-
const input = process.stdin;
|
|
5873
|
-
const previousRaw = input.isRaw;
|
|
5874
|
-
input.setRawMode(true);
|
|
5875
|
-
input.resume();
|
|
5876
|
-
input.setEncoding("utf8");
|
|
5877
|
-
try {
|
|
5878
|
-
const value = await new Promise((resolveValue, reject) => {
|
|
5879
|
-
let collected = "";
|
|
5880
|
-
const onData = (chunk) => {
|
|
5881
|
-
for (const character of chunk) {
|
|
5882
|
-
if (character === "\x03") {
|
|
5883
|
-
input.off("data", onData);
|
|
5884
|
-
reject(new Error("asset3d_api_key_input_cancelled"));
|
|
5885
|
-
return;
|
|
5886
|
-
}
|
|
5887
|
-
if (character === "\r" || character === `
|
|
5888
|
-
`) {
|
|
5889
|
-
input.off("data", onData);
|
|
5890
|
-
resolveValue(collected);
|
|
5891
|
-
return;
|
|
5892
|
-
}
|
|
5893
|
-
if (character === "" || character === "\b")
|
|
5894
|
-
collected = collected.slice(0, -1);
|
|
5895
|
-
else
|
|
5896
|
-
collected += character;
|
|
5897
|
-
}
|
|
5898
|
-
};
|
|
5899
|
-
input.on("data", onData);
|
|
5900
|
-
});
|
|
5901
|
-
process.stdout.write(`
|
|
5902
|
-
`);
|
|
5903
|
-
return validateKey(value);
|
|
5904
|
-
} finally {
|
|
5905
|
-
input.setRawMode(previousRaw ?? false);
|
|
5906
|
-
input.pause();
|
|
5907
|
-
}
|
|
5908
|
-
}
|
|
5909
|
-
async function acquireAwKey(path) {
|
|
5910
|
-
const fromEnvironment = process.env[AW_KEY_ENV];
|
|
5911
|
-
if (fromEnvironment)
|
|
5912
|
-
return { key: validateKey(fromEnvironment), source: "environment" };
|
|
5913
|
-
const stored = readAwCredential(path);
|
|
5914
|
-
if (stored)
|
|
5915
|
-
return { key: stored, source: "stored" };
|
|
5916
|
-
return { key: await promptAwKey(), source: "prompt" };
|
|
5917
|
-
}
|
|
5918
|
-
function writeAwCredential(pathInput, keyInput) {
|
|
5919
|
-
if (!isAbsolute6(pathInput))
|
|
5920
|
-
throw new Error("asset3d_credential_path_invalid: absolute path required");
|
|
5921
|
-
const path = resolve14(pathInput);
|
|
5922
|
-
const key = validateKey(keyInput);
|
|
5923
|
-
const parent = dirname11(path);
|
|
5924
|
-
ensurePrivateDir(parent);
|
|
5925
|
-
const parentMetadata = lstatSync10(parent);
|
|
5926
|
-
const wrongParentOwner = typeof process.getuid === "function" && parentMetadata.uid !== process.getuid();
|
|
5927
|
-
if (!parentMetadata.isDirectory() || parentMetadata.isSymbolicLink() || wrongParentOwner) {
|
|
5928
|
-
throw new Error("asset3d_credential_path_invalid: parent must be an owned regular directory");
|
|
5929
|
-
}
|
|
5930
|
-
chmodSync5(parent, 448);
|
|
5931
|
-
const existed = existsSync11(path);
|
|
5932
|
-
if (existed)
|
|
5933
|
-
readAwCredential(path);
|
|
5934
|
-
const previous = existed ? readFileSync15(path) : undefined;
|
|
5935
|
-
const previousMode = existed ? statSync7(path).mode & 511 : undefined;
|
|
5936
|
-
const bytes = `${JSON.stringify({ schemaVersion: AW_CREDENTIAL_SCHEMA, provider: "aw", sandboxKey: key }, null, 2)}
|
|
5937
|
-
`;
|
|
5938
|
-
const changed = !previous || !previous.equals(Buffer.from(bytes));
|
|
5939
|
-
if (changed)
|
|
5940
|
-
atomicWrite(path, bytes, 384);
|
|
5941
|
-
chmodSync5(path, 384);
|
|
5942
|
-
let active = true;
|
|
5943
|
-
return {
|
|
5944
|
-
path,
|
|
5945
|
-
changed,
|
|
5946
|
-
commit() {
|
|
5947
|
-
active = false;
|
|
5948
|
-
},
|
|
5949
|
-
rollback() {
|
|
5950
|
-
if (!active || !changed)
|
|
5951
|
-
return;
|
|
5952
|
-
if (previous) {
|
|
5953
|
-
atomicWrite(path, previous, previousMode ?? 384);
|
|
5954
|
-
chmodSync5(path, previousMode ?? 384);
|
|
5955
|
-
} else {
|
|
5956
|
-
unlinkSync4(path);
|
|
5957
|
-
}
|
|
5958
|
-
active = false;
|
|
5959
|
-
}
|
|
5960
|
-
};
|
|
5961
|
-
}
|
|
5962
|
-
|
|
5963
|
-
// src/asset3d/mcp-proxy.ts
|
|
5964
|
-
import { spawn as spawn4 } from "node:child_process";
|
|
5965
|
-
async function runDormantAsset3dMcp() {
|
|
5966
|
-
let buffer = "";
|
|
5967
|
-
process.stdin.setEncoding("utf8");
|
|
5968
|
-
for await (const chunk of process.stdin) {
|
|
5969
|
-
buffer += chunk;
|
|
5970
|
-
for (;; ) {
|
|
5971
|
-
const newline = buffer.indexOf(`
|
|
5972
|
-
`);
|
|
5973
|
-
if (newline < 0)
|
|
5974
|
-
break;
|
|
5975
|
-
const line = buffer.slice(0, newline);
|
|
5976
|
-
buffer = buffer.slice(newline + 1);
|
|
5977
|
-
if (!line.trim())
|
|
5978
|
-
continue;
|
|
5979
|
-
let request2;
|
|
5980
|
-
try {
|
|
5981
|
-
request2 = JSON.parse(line);
|
|
5982
|
-
} catch {
|
|
5983
|
-
process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } })}
|
|
5984
|
-
`);
|
|
5985
|
-
continue;
|
|
5986
|
-
}
|
|
5987
|
-
if (request2.id === undefined)
|
|
5988
|
-
continue;
|
|
5989
|
-
let response;
|
|
5990
|
-
if (request2.method === "initialize") {
|
|
5991
|
-
const params = request2.params;
|
|
5992
|
-
const protocolVersion = params && typeof params === "object" && !Array.isArray(params) && typeof params.protocolVersion === "string" ? params.protocolVersion : "2024-11-05";
|
|
5993
|
-
response = {
|
|
5994
|
-
jsonrpc: "2.0",
|
|
5995
|
-
id: request2.id,
|
|
5996
|
-
result: {
|
|
5997
|
-
protocolVersion,
|
|
5998
|
-
capabilities: { tools: {} },
|
|
5999
|
-
serverInfo: { name: "asset3d-search", version: "0.3.5" }
|
|
6000
|
-
}
|
|
6001
|
-
};
|
|
6002
|
-
} else if (request2.method === "tools/list") {
|
|
6003
|
-
response = { jsonrpc: "2.0", id: request2.id, result: { tools: [] } };
|
|
6004
|
-
} else {
|
|
6005
|
-
response = { jsonrpc: "2.0", id: request2.id, error: { code: -32601, message: "Method not found" } };
|
|
6006
|
-
}
|
|
6007
|
-
process.stdout.write(`${JSON.stringify(response)}
|
|
6008
|
-
`);
|
|
6009
|
-
}
|
|
6010
|
-
}
|
|
6011
|
-
return 0;
|
|
6012
|
-
}
|
|
6013
|
-
function requestKey(id) {
|
|
6014
|
-
return typeof id === "string" || typeof id === "number" ? JSON.stringify(id) : undefined;
|
|
6015
|
-
}
|
|
6016
|
-
function searchToolError(id, error) {
|
|
6017
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
6018
|
-
return {
|
|
6019
|
-
jsonrpc: "2.0",
|
|
6020
|
-
id,
|
|
6021
|
-
result: {
|
|
6022
|
-
content: [{ type: "text", text: message.slice(0, 512) }],
|
|
6023
|
-
isError: true
|
|
6024
|
-
}
|
|
6025
|
-
};
|
|
6026
|
-
}
|
|
6027
|
-
function rewriteSearchTool(tool) {
|
|
6028
|
-
if (tool.name !== "search_asset")
|
|
6029
|
-
return tool;
|
|
6030
|
-
const schema = tool.inputSchema;
|
|
6031
|
-
if (!schema || typeof schema !== "object" || Array.isArray(schema))
|
|
6032
|
-
return tool;
|
|
6033
|
-
const inputSchema = schema;
|
|
6034
|
-
const properties = inputSchema.properties;
|
|
6035
|
-
if (!properties || typeof properties !== "object" || Array.isArray(properties))
|
|
6036
|
-
return tool;
|
|
6037
|
-
const { output_dir: _outputDir, ...safeProperties } = properties;
|
|
6038
|
-
const required = Array.isArray(inputSchema.required) ? inputSchema.required.filter((field) => typeof field === "string" && field !== "output_dir") : [];
|
|
6039
|
-
return {
|
|
6040
|
-
...tool,
|
|
6041
|
-
description: `${typeof tool.description === "string" ? `${tool.description} ` : ""}Run forgeax-game asset3d begin first and pass its execution ID. The bridge injects the transaction-owned output directory. For native Pack preservation select output_format=asset when advertised; glb explicitly requests conversion.`,
|
|
6042
|
-
inputSchema: {
|
|
6043
|
-
...inputSchema,
|
|
6044
|
-
properties: {
|
|
6045
|
-
...safeProperties,
|
|
6046
|
-
execution: {
|
|
6047
|
-
type: "string",
|
|
6048
|
-
pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
|
|
6049
|
-
description: "Execution ID returned by forgeax-game asset3d begin for these exact queries."
|
|
6050
|
-
}
|
|
6051
|
-
},
|
|
6052
|
-
required: [...new Set([...required, "execution"])]
|
|
6053
|
-
}
|
|
6054
|
-
};
|
|
6055
|
-
}
|
|
6056
|
-
function rewriteAsset3dToolsList(response) {
|
|
6057
|
-
const result = response.result;
|
|
6058
|
-
if (!result || typeof result !== "object" || Array.isArray(result))
|
|
6059
|
-
return response;
|
|
6060
|
-
const tools = result.tools;
|
|
6061
|
-
if (!Array.isArray(tools))
|
|
6062
|
-
return response;
|
|
6063
|
-
return {
|
|
6064
|
-
...response,
|
|
6065
|
-
result: {
|
|
6066
|
-
...result,
|
|
6067
|
-
tools: tools.map((tool) => tool && typeof tool === "object" && !Array.isArray(tool) ? rewriteSearchTool(tool) : tool)
|
|
6068
|
-
}
|
|
6069
|
-
};
|
|
6070
|
-
}
|
|
6071
|
-
function transformAsset3dSearchCall(projectRoot, request2) {
|
|
6072
|
-
if (request2.method !== "tools/call")
|
|
6073
|
-
return request2;
|
|
6074
|
-
const params = request2.params;
|
|
6075
|
-
if (!params || typeof params !== "object" || Array.isArray(params))
|
|
6076
|
-
return request2;
|
|
6077
|
-
const call = params;
|
|
6078
|
-
if (call.name !== "search_asset")
|
|
6079
|
-
return request2;
|
|
6080
|
-
const rawArguments = call.arguments;
|
|
6081
|
-
if (!rawArguments || typeof rawArguments !== "object" || Array.isArray(rawArguments)) {
|
|
6082
|
-
throw new Error("asset3d_search_arguments_invalid");
|
|
6083
|
-
}
|
|
6084
|
-
const args = rawArguments;
|
|
6085
|
-
if (typeof args.execution !== "string") {
|
|
6086
|
-
throw new Error("asset3d_execution_required: run `forgeax-game asset3d begin` first");
|
|
6087
|
-
}
|
|
6088
|
-
if (!Array.isArray(args.queries)) {
|
|
6089
|
-
throw new Error("asset3d_queries_invalid");
|
|
6090
|
-
}
|
|
6091
|
-
const queries = args.queries.map((query) => {
|
|
6092
|
-
if (typeof query === "string")
|
|
6093
|
-
return query;
|
|
6094
|
-
if (query && typeof query === "object" && !Array.isArray(query)) {
|
|
6095
|
-
const selected = query;
|
|
6096
|
-
if (Object.keys(selected).every((key) => key === "content" || key === "assetId") && typeof selected.content === "string" && typeof selected.assetId === "string" && /^[A-Za-z0-9._-]{1,128}$/.test(selected.assetId) && ![".", ".."].includes(selected.assetId)) {
|
|
6097
|
-
return selected.content;
|
|
6098
|
-
}
|
|
6099
|
-
}
|
|
6100
|
-
throw new Error("asset3d_queries_invalid");
|
|
6101
|
-
});
|
|
6102
|
-
const outputDir = asset3dSearchOutputDir(projectRoot, args.execution, queries);
|
|
6103
|
-
const { execution: _execution, output_dir: _callerOutput, ...providerArguments } = args;
|
|
6104
|
-
return {
|
|
6105
|
-
...request2,
|
|
6106
|
-
params: {
|
|
6107
|
-
...call,
|
|
6108
|
-
arguments: {
|
|
6109
|
-
...providerArguments,
|
|
6110
|
-
output_dir: outputDir
|
|
6111
|
-
}
|
|
6112
|
-
}
|
|
6113
|
-
};
|
|
6114
|
-
}
|
|
6115
|
-
async function runAsset3dMcpProxy(projectRoot, launch) {
|
|
6116
|
-
const child = spawn4(launch.command, [...launch.args], {
|
|
6117
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
6118
|
-
env: { ...process.env, ...launch.env }
|
|
6119
|
-
});
|
|
6120
|
-
child.stderr.pipe(process.stderr);
|
|
6121
|
-
const toolsListRequests = new Set;
|
|
6122
|
-
const searchExecutions = new Map;
|
|
6123
|
-
let inputBuffer = "";
|
|
6124
|
-
let outputBuffer = "";
|
|
6125
|
-
process.stdin.setEncoding("utf8");
|
|
6126
|
-
process.stdin.on("data", (chunk) => {
|
|
6127
|
-
inputBuffer += chunk;
|
|
6128
|
-
for (;; ) {
|
|
6129
|
-
const newline = inputBuffer.indexOf(`
|
|
6130
|
-
`);
|
|
6131
|
-
if (newline < 0)
|
|
6132
|
-
break;
|
|
6133
|
-
const line = inputBuffer.slice(0, newline);
|
|
6134
|
-
inputBuffer = inputBuffer.slice(newline + 1);
|
|
6135
|
-
if (!line.trim())
|
|
6136
|
-
continue;
|
|
6137
|
-
let request2;
|
|
6138
|
-
try {
|
|
6139
|
-
request2 = JSON.parse(line);
|
|
6140
|
-
const key = requestKey(request2.id);
|
|
6141
|
-
if (request2.method === "tools/list" && key)
|
|
6142
|
-
toolsListRequests.add(key);
|
|
6143
|
-
const params = request2.params;
|
|
6144
|
-
if (key && request2.method === "tools/call" && params && typeof params === "object" && !Array.isArray(params) && params.name === "search_asset") {
|
|
6145
|
-
const args = params.arguments;
|
|
6146
|
-
if (args && typeof args === "object" && !Array.isArray(args) && typeof args.execution === "string") {
|
|
6147
|
-
searchExecutions.set(key, args.execution);
|
|
6148
|
-
}
|
|
6149
|
-
}
|
|
6150
|
-
request2 = transformAsset3dSearchCall(projectRoot, request2);
|
|
6151
|
-
} catch (error) {
|
|
6152
|
-
let id = null;
|
|
6153
|
-
try {
|
|
6154
|
-
id = JSON.parse(line).id ?? null;
|
|
6155
|
-
} catch {}
|
|
6156
|
-
process.stdout.write(`${JSON.stringify(searchToolError(id, error))}
|
|
6157
|
-
`);
|
|
6158
|
-
continue;
|
|
6159
|
-
}
|
|
6160
|
-
child.stdin.write(`${JSON.stringify(request2)}
|
|
6161
|
-
`);
|
|
6162
|
-
}
|
|
6163
|
-
});
|
|
6164
|
-
process.stdin.on("end", () => child.stdin.end());
|
|
6165
|
-
child.stdout.setEncoding("utf8");
|
|
6166
|
-
child.stdout.on("data", (chunk) => {
|
|
6167
|
-
outputBuffer += chunk;
|
|
6168
|
-
for (;; ) {
|
|
6169
|
-
const newline = outputBuffer.indexOf(`
|
|
6170
|
-
`);
|
|
6171
|
-
if (newline < 0)
|
|
6172
|
-
break;
|
|
6173
|
-
const line = outputBuffer.slice(0, newline);
|
|
6174
|
-
outputBuffer = outputBuffer.slice(newline + 1);
|
|
6175
|
-
if (!line.trim())
|
|
6176
|
-
continue;
|
|
6177
|
-
try {
|
|
6178
|
-
let response = JSON.parse(line);
|
|
6179
|
-
const key = requestKey(response.id);
|
|
6180
|
-
if (key && toolsListRequests.delete(key))
|
|
6181
|
-
response = rewriteAsset3dToolsList(response);
|
|
6182
|
-
const execution = key ? searchExecutions.get(key) : undefined;
|
|
6183
|
-
if (key && execution) {
|
|
6184
|
-
searchExecutions.delete(key);
|
|
6185
|
-
try {
|
|
6186
|
-
const result = response.result;
|
|
6187
|
-
if (!result || typeof result !== "object" || Array.isArray(result) || result.isError === true) {
|
|
6188
|
-
throw new Error("asset3d_provider_result_missing");
|
|
6189
|
-
}
|
|
6190
|
-
const content = result.content;
|
|
6191
|
-
if (!Array.isArray(content) || content.length !== 1) {
|
|
6192
|
-
throw new Error("asset3d_provider_result_invalid");
|
|
6193
|
-
}
|
|
6194
|
-
const text2 = content[0];
|
|
6195
|
-
if (!text2 || typeof text2 !== "object" || Array.isArray(text2) || text2.type !== "text" || typeof text2.text !== "string") {
|
|
6196
|
-
throw new Error("asset3d_provider_result_invalid");
|
|
6197
|
-
}
|
|
6198
|
-
recordAsset3dProviderResult(projectRoot, execution, text2.text);
|
|
6199
|
-
} catch (error) {
|
|
6200
|
-
response = searchToolError(response.id, error);
|
|
6201
|
-
}
|
|
6202
|
-
}
|
|
6203
|
-
process.stdout.write(`${JSON.stringify(response)}
|
|
6204
|
-
`);
|
|
6205
|
-
} catch {
|
|
6206
|
-
process.stderr.write(`asset3d_provider_mcp_stdout_invalid
|
|
6207
|
-
`);
|
|
6208
|
-
child.kill("SIGTERM");
|
|
6209
|
-
}
|
|
6210
|
-
}
|
|
6211
|
-
});
|
|
6212
|
-
const stopChild = () => {
|
|
6213
|
-
if (child.exitCode === null && child.signalCode === null)
|
|
6214
|
-
child.kill("SIGTERM");
|
|
6215
|
-
};
|
|
6216
|
-
process.once("SIGINT", stopChild);
|
|
6217
|
-
process.once("SIGTERM", stopChild);
|
|
6218
|
-
return await new Promise((resolve15, reject) => {
|
|
6219
|
-
child.once("error", reject);
|
|
6220
|
-
child.once("close", (code, signal) => resolve15(code ?? (signal ? 1 : 0)));
|
|
6221
|
-
});
|
|
6222
|
-
}
|
|
6223
|
-
|
|
6224
|
-
// src/asset3d/host-config.ts
|
|
6225
|
-
function asset3dHostMode(client, root) {
|
|
6226
|
-
return inspectConfig(client, root, launchSpec("local")).state === "current" ? "local" : "npx";
|
|
6227
|
-
}
|
|
6228
|
-
function inspectAsset3dHost(client, root, mode = asset3dHostMode(client, root)) {
|
|
6229
|
-
const launch = asset3dLaunchSpec(mode);
|
|
6230
|
-
const status = inspectConfig(client, root, launch, ASSET3D_SERVER_KEY);
|
|
6231
|
-
if (status.state !== "different")
|
|
6232
|
-
return status;
|
|
6233
|
-
const published = asset3dLaunchSpec("npx");
|
|
6234
|
-
if (mode === "local" && inspectConfig(client, root, published, ASSET3D_SERVER_KEY).state === "current") {
|
|
6235
|
-
return { ...status, state: "outdated" };
|
|
6236
|
-
}
|
|
6237
|
-
const previous = configuredGameVersion(client, root, ASSET3D_SERVER_KEY);
|
|
6238
|
-
const current = published.args.find((arg) => arg.startsWith("@forgeax/game@"))?.slice("@forgeax/game@".length);
|
|
6239
|
-
if (!previous || !current || !/^\d+\.\d+\.\d+$/.test(previous) || !/^\d+\.\d+\.\d+$/.test(current))
|
|
6240
|
-
return status;
|
|
6241
|
-
const oldParts = previous.split(".").map(Number);
|
|
6242
|
-
const newParts = current.split(".").map(Number);
|
|
6243
|
-
const differing = oldParts.findIndex((part, index) => part !== newParts[index]);
|
|
6244
|
-
if (differing < 0 || oldParts[differing] >= newParts[differing])
|
|
6245
|
-
return status;
|
|
6246
|
-
const oldLaunch = { ...published, args: published.args.map((arg) => arg === `@forgeax/game@${current}` ? `@forgeax/game@${previous}` : arg) };
|
|
6247
|
-
if (inspectConfig(client, root, oldLaunch, ASSET3D_SERVER_KEY).state !== "current")
|
|
6248
|
-
return status;
|
|
6249
|
-
return { ...status, state: "outdated" };
|
|
6250
|
-
}
|
|
6251
|
-
function refreshAsset3dHost(client, root) {
|
|
6252
|
-
const status = inspectAsset3dHost(client, root);
|
|
6253
|
-
if (status.state === "outdated") {
|
|
6254
|
-
const result = applyConfig(client, root, asset3dLaunchSpec(asset3dHostMode(client, root)), ASSET3D_SERVER_KEY);
|
|
6255
|
-
process.stdout.write(`UPDATED ${client.label} Asset3D bridge: ${result.path}
|
|
6256
|
-
`);
|
|
6257
|
-
} else if (status.state === "different" || status.state === "invalid") {
|
|
6258
|
-
process.stderr.write(`WARN ${client.label}: existing ${ASSET3D_SERVER_KEY} configuration was preserved; it is not a recognized older package launcher.
|
|
6259
|
-
`);
|
|
6260
|
-
}
|
|
6261
|
-
}
|
|
6262
|
-
|
|
6263
|
-
// src/devkit/engine-mounts.ts
|
|
6264
|
-
import { lstatSync as lstatSync11, readFileSync as readFileSync16, readdirSync as readdirSync8, readlinkSync as readlinkSync2, rmdirSync, unlinkSync as unlinkSync5, writeFileSync as writeFileSync11 } from "node:fs";
|
|
6265
|
-
import { dirname as dirname12, join as join11, resolve as resolve15 } from "node:path";
|
|
6266
|
-
var HOSTS = {
|
|
6267
|
-
".agents/skills": ["codex"],
|
|
6268
|
-
".claude/skills": ["claude"],
|
|
6269
|
-
".cursor/skills": ["cursor"],
|
|
6270
|
-
".codebuddy/skills": ["codebuddy", "workbuddy"],
|
|
6271
|
-
".workbuddy/skills": ["workbuddy"]
|
|
6272
|
-
};
|
|
6273
|
-
function pruneUnselectedEngineMounts(root, clients) {
|
|
6274
|
-
const manifestPath = join11(root, ".forgeax", "skill-install-manifest.json");
|
|
6275
|
-
const regular = (path) => {
|
|
6276
|
-
try {
|
|
6277
|
-
const stat = lstatSync11(path);
|
|
6278
|
-
return stat.isFile() && !stat.isSymbolicLink();
|
|
6279
|
-
} catch {
|
|
6280
|
-
return false;
|
|
6281
|
-
}
|
|
6282
|
-
};
|
|
6283
|
-
const directory = (path) => {
|
|
6284
|
-
try {
|
|
6285
|
-
const stat = lstatSync11(path);
|
|
6286
|
-
return stat.isDirectory() && !stat.isSymbolicLink();
|
|
6287
|
-
} catch {
|
|
6288
|
-
return false;
|
|
6289
|
-
}
|
|
6290
|
-
};
|
|
6291
|
-
if (!directory(join11(root, ".forgeax")) || !regular(manifestPath))
|
|
6292
|
-
return [];
|
|
6293
|
-
let manifest;
|
|
6294
|
-
try {
|
|
6295
|
-
manifest = JSON.parse(readFileSync16(manifestPath, "utf8"));
|
|
6296
|
-
} catch {
|
|
6297
|
-
return [];
|
|
6298
|
-
}
|
|
6299
|
-
if (!manifest || manifest.schemaVersion !== "1.0.0" || manifest.sourceRoot !== "skills" || !Array.isArray(manifest.mounts))
|
|
6300
|
-
return [];
|
|
6301
|
-
const removed = [];
|
|
6302
|
-
for (const mount of manifest.mounts) {
|
|
6303
|
-
if (!mount || typeof mount.root !== "string" || !Object.hasOwn(HOSTS, mount.root))
|
|
6304
|
-
continue;
|
|
6305
|
-
const hosts = HOSTS[mount.root];
|
|
6306
|
-
if (!hosts || hosts.some((host) => clients.includes(host)) || !Array.isArray(mount.skills))
|
|
6307
|
-
continue;
|
|
6308
|
-
if (!mount.skills.length || !mount.skills.every((id) => /^forgeax-engine-[a-z0-9-]+$/.test(id)) || new Set(mount.skills).size !== mount.skills.length)
|
|
6309
|
-
continue;
|
|
6310
|
-
const path = join11(root, mount.root);
|
|
6311
|
-
if (!directory(dirname12(path)) || !directory(path))
|
|
6312
|
-
continue;
|
|
6313
|
-
const expected = [".gitignore", ...mount.skills].sort();
|
|
6314
|
-
if (JSON.stringify(readdirSync8(path).sort()) !== JSON.stringify(expected))
|
|
6315
|
-
continue;
|
|
6316
|
-
const ignore = join11(path, ".gitignore");
|
|
6317
|
-
const expectedIgnore = ["# BEGIN FORGEAX MANAGED SKILLS", ...mount.skills.map((id) => `/${id}`), "# END FORGEAX MANAGED SKILLS", ""].join(`
|
|
6318
|
-
`);
|
|
6319
|
-
if (!regular(ignore) || readFileSync16(ignore, "utf8") !== expectedIgnore)
|
|
6320
|
-
continue;
|
|
6321
|
-
if (!mount.skills.every((id) => {
|
|
6322
|
-
const link = join11(path, id);
|
|
6323
|
-
return lstatSync11(link).isSymbolicLink() && resolve15(path, readlinkSync2(link)) === resolve15(root, "skills", id);
|
|
6324
|
-
}))
|
|
6325
|
-
continue;
|
|
6326
|
-
for (const id of mount.skills)
|
|
6327
|
-
unlinkSync5(join11(path, id));
|
|
6328
|
-
unlinkSync5(ignore);
|
|
6329
|
-
rmdirSync(path);
|
|
6330
|
-
if (readdirSync8(dirname12(path)).length === 0)
|
|
6331
|
-
rmdirSync(dirname12(path));
|
|
6332
|
-
removed.push(mount.root);
|
|
6333
|
-
}
|
|
6334
|
-
if (removed.length) {
|
|
6335
|
-
manifest.mounts = manifest.mounts.filter((mount) => !mount || !removed.includes(mount.root));
|
|
6336
|
-
writeFileSync11(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
6337
|
-
`);
|
|
6338
|
-
}
|
|
6339
|
-
return removed;
|
|
6340
|
-
}
|
|
6341
|
-
|
|
6342
|
-
// src/cli/dispatch.ts
|
|
6343
|
-
var HELP = `ForgeaX game development plugin
|
|
6344
|
-
|
|
6345
|
-
Usage:
|
|
6346
|
-
forgeax-game install [--ide ${CLIENT_CHOICES.join(",")}] [--local]
|
|
6347
|
-
forgeax-game uninstall [--ide ...] [--purge]
|
|
6348
|
-
forgeax-game init
|
|
6349
|
-
forgeax-game use <slug>
|
|
6350
|
-
forgeax-game doctor
|
|
6351
|
-
forgeax-game preview stop [--game <slug>] [--target-dir <path>] [--json]
|
|
6352
|
-
forgeax-game devkit install
|
|
6353
|
-
forgeax-game agents update
|
|
6354
|
-
forgeax-game asset3d enable [--library aw|ea] [--base-url <gateway/service URL>] [--ide ${CLIENT_CHOICES.join(",")}]
|
|
6355
|
-
forgeax-game asset3d install --provider-bundle <archive> --sha256 <hex> --download-origin <scheme://host:port> [--aw-base-url <URL> --aw-credential-file <absolute path>] [...]
|
|
6356
|
-
forgeax-game asset3d uninstall
|
|
6357
|
-
forgeax-game asset3d begin --query <text> [--query <text> ...] --json
|
|
6358
|
-
forgeax-game asset3d commit --execution <uuid> --json [--provider-result-stdin] [--refresh]
|
|
6359
|
-
forgeax-game asset3d abort --execution <uuid> --json
|
|
6360
|
-
forgeax-game asset3d doctor --json
|
|
6361
|
-
forgeax-game update [--ide ...]
|
|
6362
|
-
forgeax-game version
|
|
6363
|
-
forgeax-game help
|
|
6364
|
-
|
|
6365
|
-
With no arguments, forgeax-game runs the stdio MCP server.
|
|
6366
|
-
`;
|
|
6367
|
-
function parseInstallArgs(args) {
|
|
6368
|
-
let mode = "npx";
|
|
6369
|
-
let ids;
|
|
6370
|
-
for (let i = 0;i < args.length; i++) {
|
|
6371
|
-
const arg = args[i];
|
|
6372
|
-
if (arg === "--local") {
|
|
6373
|
-
mode = "local";
|
|
6374
|
-
continue;
|
|
6375
|
-
}
|
|
6376
|
-
if (arg === "--ide") {
|
|
6377
|
-
const value = args[++i];
|
|
6378
|
-
if (!value)
|
|
6379
|
-
throw new Error("--ide requires a comma-separated client list");
|
|
6380
|
-
ids = value.split(",").map((id) => id.trim()).filter(Boolean);
|
|
6381
|
-
continue;
|
|
6382
|
-
}
|
|
6383
|
-
if (arg.startsWith("--ide=")) {
|
|
6384
|
-
ids = arg.slice("--ide=".length).split(",").map((id) => id.trim()).filter(Boolean);
|
|
6385
|
-
continue;
|
|
6386
|
-
}
|
|
6387
|
-
throw new Error(`unknown install option: ${arg}`);
|
|
6388
|
-
}
|
|
6389
|
-
const selected = ids ?? [...CLIENT_IDS];
|
|
6390
|
-
if (selected.length === 0)
|
|
6391
|
-
throw new Error("--ide did not name any clients");
|
|
6392
|
-
const uniqueNames = [...new Set(selected)];
|
|
6393
|
-
const unknown = uniqueNames.filter((id) => !findClient(id));
|
|
6394
|
-
if (unknown.length) {
|
|
6395
|
-
throw new Error(`unknown client${unknown.length === 1 ? "" : "s"}: ${unknown.join(", ")}. Choose from ${CLIENT_CHOICES.join(", ")}.`);
|
|
6396
|
-
}
|
|
6397
|
-
const clients = uniqueNames.map((id) => findClient(id));
|
|
6398
|
-
return { clients: [...new Map(clients.map((client) => [client.id, client])).values()], mode };
|
|
6399
|
-
}
|
|
6400
|
-
function requireProject() {
|
|
6401
|
-
const project = resolveProject();
|
|
6402
|
-
if (!project.root) {
|
|
6403
|
-
throw new Error(`no released Engine game found searching upward from ${project.searchedFrom}; run this command inside a game created by the released Engine SDK`);
|
|
6404
|
-
}
|
|
6405
|
-
return project.root;
|
|
4746
|
+
return project.root;
|
|
6406
4747
|
}
|
|
6407
4748
|
function updateAgentsFile(root) {
|
|
6408
|
-
const path =
|
|
6409
|
-
const existing =
|
|
4749
|
+
const path = join11(root, "AGENTS.md");
|
|
4750
|
+
const existing = existsSync9(path) ? readFileSync14(path, "utf8") : undefined;
|
|
6410
4751
|
const content = upsertBlock(existing, ROUTING_TEXT);
|
|
6411
4752
|
if (content === existing)
|
|
6412
4753
|
return { path, changed: false };
|
|
6413
|
-
|
|
4754
|
+
writeFileSync10(path, content);
|
|
6414
4755
|
return { path, changed: true };
|
|
6415
4756
|
}
|
|
6416
4757
|
function removeAgentsBlock(root) {
|
|
6417
|
-
const path =
|
|
6418
|
-
if (!
|
|
4758
|
+
const path = join11(root, "AGENTS.md");
|
|
4759
|
+
if (!existsSync9(path))
|
|
6419
4760
|
return { path, changed: false };
|
|
6420
|
-
const existing =
|
|
4761
|
+
const existing = readFileSync14(path, "utf8");
|
|
6421
4762
|
const content = removeBlock(existing);
|
|
6422
4763
|
if (content === existing)
|
|
6423
4764
|
return { path, changed: false };
|
|
6424
|
-
|
|
4765
|
+
writeFileSync10(path, content);
|
|
6425
4766
|
return { path, changed: true };
|
|
6426
4767
|
}
|
|
6427
4768
|
async function installCommand(args) {
|
|
@@ -6443,7 +4784,7 @@ async function installCommand(args) {
|
|
|
6443
4784
|
}
|
|
6444
4785
|
try {
|
|
6445
4786
|
const result = applyConfig(client, project.root ?? process.cwd(), launch);
|
|
6446
|
-
|
|
4787
|
+
retireAsset3dHost(client, project.root ?? process.cwd());
|
|
6447
4788
|
process.stdout.write(`${result.changed ? "UPDATED" : "CURRENT"} ${client.label}: ${result.path}${result.backup ? ` (backup: ${result.backup})` : ""}
|
|
6448
4789
|
`);
|
|
6449
4790
|
if (client.postInstallNote)
|
|
@@ -6525,7 +4866,7 @@ async function useCommand(args) {
|
|
|
6525
4866
|
throw new Error(`game ${JSON.stringify(slug)} not found. Available: ${listGames(root).join(", ") || "(none)"}`);
|
|
6526
4867
|
}
|
|
6527
4868
|
if (listGames(root).length > 1) {
|
|
6528
|
-
|
|
4869
|
+
writeFileSync10(join11(root, ".forgeax", "active-game.json"), `${JSON.stringify({ version: 1, slug }, null, 2)}
|
|
6529
4870
|
`, "utf8");
|
|
6530
4871
|
}
|
|
6531
4872
|
process.stdout.write(`Active game: ${slug}
|
|
@@ -6538,11 +4879,11 @@ async function previewCommand(args) {
|
|
|
6538
4879
|
}
|
|
6539
4880
|
let requested;
|
|
6540
4881
|
let targetDir;
|
|
6541
|
-
let
|
|
4882
|
+
let json3 = false;
|
|
6542
4883
|
for (let index = 1;index < args.length; index++) {
|
|
6543
4884
|
const arg = args[index];
|
|
6544
4885
|
if (arg === "--json") {
|
|
6545
|
-
|
|
4886
|
+
json3 = true;
|
|
6546
4887
|
continue;
|
|
6547
4888
|
}
|
|
6548
4889
|
if (arg === "--game" || arg === "--target-dir") {
|
|
@@ -6566,7 +4907,7 @@ async function previewCommand(args) {
|
|
|
6566
4907
|
throw new Error("no matching Engine game found");
|
|
6567
4908
|
const result = await stopEnginePreview(project.root, selectedGame2);
|
|
6568
4909
|
const envelope = { schemaVersion: "1.0.0", command: "preview.stop", ok: true, value: { game: slug, stopped: result.stopped, stateFile: result.paths.state } };
|
|
6569
|
-
process.stdout.write(
|
|
4910
|
+
process.stdout.write(json3 ? `${JSON.stringify(envelope)}
|
|
6570
4911
|
` : `${result.stopped ? "Stopped" : "No live"} Engine Preview for ${slug}.
|
|
6571
4912
|
`);
|
|
6572
4913
|
return 0;
|
|
@@ -6602,13 +4943,34 @@ function configuredClientIds(projectRoot) {
|
|
|
6602
4943
|
}
|
|
6603
4944
|
async function uninstallCommand(args) {
|
|
6604
4945
|
const purge = args.includes("--purge");
|
|
6605
|
-
const
|
|
4946
|
+
const allProjects = args.includes("--all-projects");
|
|
4947
|
+
const rest = args.filter((arg) => arg !== "--purge" && arg !== "--all-projects");
|
|
6606
4948
|
const requested = parseIdeSelector(rest, "usage: forgeax-game uninstall [--ide codex,claude,...] [--purge]");
|
|
6607
4949
|
const binding = resolveProject();
|
|
6608
4950
|
const root = binding.root;
|
|
6609
4951
|
const targets = requested ? requested.map((id) => id === "workbuddy" ? "codebuddy" : id) : root ? configuredClientIds(root) : [...CLIENT_IDS];
|
|
6610
4952
|
const clients = CLIENTS.filter((client) => targets.includes(client.id));
|
|
6611
4953
|
let failures = 0;
|
|
4954
|
+
const projects = allProjects ? registeredProjects() : root ? [root] : [];
|
|
4955
|
+
for (const project of projects) {
|
|
4956
|
+
try {
|
|
4957
|
+
const removed = disableAllExtensions(project);
|
|
4958
|
+
process.stdout.write(`DISABLED ${removed.length} extensions: ${project}
|
|
4959
|
+
`);
|
|
4960
|
+
for (const item of removed)
|
|
4961
|
+
for (const backup of item.backups)
|
|
4962
|
+
process.stdout.write(`BACKUP ${backup}
|
|
4963
|
+
`);
|
|
4964
|
+
if (allProjects && project !== root) {
|
|
4965
|
+
removeDevKit(project);
|
|
4966
|
+
removeAgentsBlock(project);
|
|
4967
|
+
}
|
|
4968
|
+
} catch (error) {
|
|
4969
|
+
failures++;
|
|
4970
|
+
process.stderr.write(`FAIL extension cleanup: ${project}: ${error instanceof Error ? error.message : String(error)}
|
|
4971
|
+
`);
|
|
4972
|
+
}
|
|
4973
|
+
}
|
|
6612
4974
|
for (const client of clients) {
|
|
6613
4975
|
try {
|
|
6614
4976
|
const result = removeConfig(client, root ?? process.cwd());
|
|
@@ -6627,7 +4989,7 @@ async function uninstallCommand(args) {
|
|
|
6627
4989
|
const agents = removeAgentsBlock(root);
|
|
6628
4990
|
process.stdout.write(`${agents.changed ? "REMOVED" : "ABSENT "} routing block: ${agents.path}
|
|
6629
4991
|
`);
|
|
6630
|
-
process.stdout.write(`KEPT your games and project metadata: ${
|
|
4992
|
+
process.stdout.write(`KEPT your games and project metadata: ${join11(root, ".forgeax")}
|
|
6631
4993
|
`);
|
|
6632
4994
|
} else {
|
|
6633
4995
|
process.stdout.write(`INFO no ForgeaX project bound; only client configuration was touched.
|
|
@@ -6791,6 +5153,15 @@ async function doctorCommand(args) {
|
|
|
6791
5153
|
return warnings === 0 ? 0 : 1;
|
|
6792
5154
|
}
|
|
6793
5155
|
var UPDATE_USAGE = "usage: forgeax-game update [--ide codex,claude,cursor,...]";
|
|
5156
|
+
function retireAsset3dHost(client, root) {
|
|
5157
|
+
const state = retireAsset3dConfig(client, root);
|
|
5158
|
+
if (state === "removed")
|
|
5159
|
+
process.stdout.write(`REMOVED ${client.label}: retired asset3d-search MCP; assets now use the project Skill + CLI. Restart the client.
|
|
5160
|
+
`);
|
|
5161
|
+
if (state === "preserved")
|
|
5162
|
+
process.stderr.write(`WARN ${client.label}: asset3d-search is not an exact recognized package launcher; preserved for manual review.
|
|
5163
|
+
`);
|
|
5164
|
+
}
|
|
6794
5165
|
function versionCommand(args) {
|
|
6795
5166
|
if (args.length > 0)
|
|
6796
5167
|
throw new Error("usage: forgeax-game version");
|
|
@@ -6827,7 +5198,7 @@ async function updateCommand(args) {
|
|
|
6827
5198
|
for (const client of configured) {
|
|
6828
5199
|
const previousVersion = configuredGameVersion(client, root);
|
|
6829
5200
|
const result = applyConfig(client, root, launch);
|
|
6830
|
-
|
|
5201
|
+
retireAsset3dHost(client, root);
|
|
6831
5202
|
process.stdout.write(`${result.changed ? "UPDATED" : "CURRENT"} ${client.label}: ${result.path} (plugin ${formatVersionTransition(previousVersion)})
|
|
6832
5203
|
`);
|
|
6833
5204
|
}
|
|
@@ -6846,312 +5217,63 @@ async function updateCommand(args) {
|
|
|
6846
5217
|
}
|
|
6847
5218
|
return 0;
|
|
6848
5219
|
}
|
|
6849
|
-
function
|
|
6850
|
-
process.stdout.write(`${JSON.stringify({ schemaVersion: "1.0.0", command, ok, ...ok ? { value: payload } : { error: payload } })}
|
|
6851
|
-
`);
|
|
6852
|
-
}
|
|
6853
|
-
async function readBoundedStdin() {
|
|
6854
|
-
const chunks = [];
|
|
6855
|
-
let total = 0;
|
|
6856
|
-
for await (const chunk of process.stdin) {
|
|
6857
|
-
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
6858
|
-
total += bytes.byteLength;
|
|
6859
|
-
if (total > MAX_JSON_BYTES)
|
|
6860
|
-
throw new Error("provider_result_too_large: stdin exceeds 1 MiB");
|
|
6861
|
-
chunks.push(bytes);
|
|
6862
|
-
}
|
|
6863
|
-
return Buffer.concat(chunks);
|
|
6864
|
-
}
|
|
6865
|
-
function canonicalAsset3dClients(value) {
|
|
6866
|
-
const values = value.split(",").map((entry) => entry.trim()).filter(Boolean).map((entry) => entry === "workbuddy" ? "codebuddy" : entry);
|
|
6867
|
-
const invalid = values.filter((entry) => !(entry in SKILL_MOUNTS));
|
|
6868
|
-
if (invalid.length)
|
|
6869
|
-
throw new Error(`asset3d_client_invalid: ${invalid.join(", ")}`);
|
|
6870
|
-
return [...new Set(values)];
|
|
6871
|
-
}
|
|
6872
|
-
function parseAssetLibraryId(value) {
|
|
6873
|
-
if (value === "aw" || value === "ea")
|
|
6874
|
-
return value;
|
|
6875
|
-
throw new Error("asset3d_library_invalid: expected aw or ea");
|
|
6876
|
-
}
|
|
6877
|
-
function parseAsset3dEnableArgs(args) {
|
|
6878
|
-
const ideArgs = [];
|
|
6879
|
-
let baseUrl;
|
|
6880
|
-
let library;
|
|
6881
|
-
for (let index = 0;index < args.length; index++) {
|
|
6882
|
-
const arg = args[index];
|
|
6883
|
-
if (arg === "--library") {
|
|
6884
|
-
const value = args[++index];
|
|
6885
|
-
if (!value)
|
|
6886
|
-
throw new Error("--library requires aw or ea");
|
|
6887
|
-
library = parseAssetLibraryId(value);
|
|
6888
|
-
continue;
|
|
6889
|
-
}
|
|
6890
|
-
if (arg.startsWith("--library=")) {
|
|
6891
|
-
library = parseAssetLibraryId(arg.slice("--library=".length));
|
|
6892
|
-
continue;
|
|
6893
|
-
}
|
|
6894
|
-
if (arg === "--base-url") {
|
|
6895
|
-
const value = args[++index];
|
|
6896
|
-
if (!value)
|
|
6897
|
-
throw new Error("--base-url requires a gateway/service URL");
|
|
6898
|
-
baseUrl = value;
|
|
6899
|
-
continue;
|
|
6900
|
-
}
|
|
6901
|
-
if (arg.startsWith("--base-url=")) {
|
|
6902
|
-
baseUrl = arg.slice("--base-url=".length);
|
|
6903
|
-
continue;
|
|
6904
|
-
}
|
|
6905
|
-
if (arg === "--ide") {
|
|
6906
|
-
const value = args[++index];
|
|
6907
|
-
if (!value)
|
|
6908
|
-
throw new Error("--ide requires a comma-separated client list");
|
|
6909
|
-
ideArgs.push("--ide", value);
|
|
6910
|
-
continue;
|
|
6911
|
-
}
|
|
6912
|
-
if (arg.startsWith("--ide=")) {
|
|
6913
|
-
ideArgs.push(arg);
|
|
6914
|
-
continue;
|
|
6915
|
-
}
|
|
6916
|
-
throw new Error("usage: forgeax-game asset3d enable [--library aw|ea] [--base-url <gateway/service URL>] [--ide codex,claude,cursor,...]");
|
|
6917
|
-
}
|
|
6918
|
-
return { requested: parseIdeSelector(ideArgs, "invalid --ide selector"), baseUrl, library };
|
|
6919
|
-
}
|
|
6920
|
-
async function asset3dCommand(args) {
|
|
5220
|
+
async function extensionCommand(id, args) {
|
|
6921
5221
|
const [operation, ...rest] = args;
|
|
6922
|
-
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
return runDormantAsset3dMcp();
|
|
6929
|
-
let launch;
|
|
6930
|
-
try {
|
|
6931
|
-
launch = asset3dProviderLaunch(binding.root);
|
|
6932
|
-
} catch (error) {
|
|
6933
|
-
if (error instanceof Error && error.message === "asset3d_not_installed") {
|
|
6934
|
-
return runDormantAsset3dMcp();
|
|
6935
|
-
}
|
|
6936
|
-
throw error;
|
|
6937
|
-
}
|
|
6938
|
-
return runAsset3dMcpProxy(binding.root, launch);
|
|
6939
|
-
}
|
|
6940
|
-
const projectRoot = requireProject();
|
|
6941
|
-
if (operation === "enable") {
|
|
6942
|
-
const parsed = parseAsset3dEnableArgs(rest);
|
|
6943
|
-
const selection = selectClients(projectRoot, parsed.requested);
|
|
6944
|
-
reportMissingClients(selection.missing);
|
|
6945
|
-
if (selection.selected.length === 0) {
|
|
6946
|
-
throw new Error("asset3d_client_missing: run `forgeax-game install --ide <client>` before enabling Asset3D");
|
|
6947
|
-
}
|
|
6948
|
-
const localMode = selection.selected.some((id) => asset3dHostMode(findClient(id), projectRoot) === "local");
|
|
6949
|
-
for (const id of selection.selected) {
|
|
6950
|
-
const client = findClient(id);
|
|
6951
|
-
if (!client)
|
|
6952
|
-
throw new Error(`asset3d_client_invalid: ${id}`);
|
|
6953
|
-
const state = inspectAsset3dHost(client, projectRoot);
|
|
6954
|
-
if (state.state === "different") {
|
|
6955
|
-
throw new Error(`asset3d_client_server_conflict: ${client.label} already has a different ${ASSET3D_SERVER_KEY} entry`);
|
|
6956
|
-
}
|
|
6957
|
-
if (state.state === "invalid") {
|
|
6958
|
-
throw new Error(`asset3d_client_config_invalid: ${client.label}: ${state.detail ?? state.path}`);
|
|
6959
|
-
}
|
|
6960
|
-
}
|
|
6961
|
-
const library = resolveAssetLibrarySelection({
|
|
6962
|
-
library: parsed.library,
|
|
6963
|
-
baseUrl: parsed.baseUrl
|
|
6964
|
-
});
|
|
6965
|
-
const os = platform2() === "win32" ? "windows" : platform2();
|
|
6966
|
-
const cpu = arch2();
|
|
6967
|
-
const target = `${os}-${cpu}`;
|
|
6968
|
-
const bundled = BUNDLED_ASSET3D_PROVIDERS[target];
|
|
6969
|
-
if (!bundled)
|
|
6970
|
-
throw new Error(`asset3d_provider_target_unreleased: ${target}`);
|
|
6971
|
-
const providerCache = resolve16(homedir5(), ".forgeax", "providers", "asset3d-search", bundled.sha256);
|
|
6972
|
-
let preparedCache = providerCache;
|
|
6973
|
-
if (!existsSync12(providerCache)) {
|
|
6974
|
-
const providerBundle = packagedAsset3dProvider(bundled.relativePath);
|
|
6975
|
-
preparedCache = prepareAsset3dProvider({ providerBundle, expectedSha256: bundled.sha256 });
|
|
6976
|
-
}
|
|
6977
|
-
const credentialFile = defaultAwCredentialFile();
|
|
6978
|
-
const credential = await acquireAwKey(credentialFile);
|
|
6979
|
-
const credentialWrite = writeAwCredential(credentialFile, credential.key);
|
|
6980
|
-
const clients = selection.selected.filter((id) => (id in SKILL_MOUNTS));
|
|
6981
|
-
try {
|
|
6982
|
-
const access = checkAssetLibraryProviderAccess({
|
|
6983
|
-
providerCache: preparedCache,
|
|
6984
|
-
depotName: library.library,
|
|
6985
|
-
serviceRoot: library.serviceRoot,
|
|
6986
|
-
credentialFile
|
|
6987
|
-
});
|
|
6988
|
-
const result = await installProvisionedAsset3d({
|
|
6989
|
-
projectRoot,
|
|
6990
|
-
providerCache: preparedCache,
|
|
6991
|
-
expectedSha256: bundled.sha256,
|
|
6992
|
-
downloadOrigins: access.downloadOrigins,
|
|
6993
|
-
awApiBaseUrl: access.serviceRoot,
|
|
6994
|
-
awDepotName: library.library,
|
|
6995
|
-
awCredentialFile: credentialFile,
|
|
6996
|
-
clients,
|
|
6997
|
-
gamePluginLaunch: launchSpec(localMode ? "local" : "npx"),
|
|
6998
|
-
replaceOwned: true
|
|
6999
|
-
});
|
|
7000
|
-
for (const id of selection.selected) {
|
|
7001
|
-
const client = findClient(id);
|
|
7002
|
-
if (!client)
|
|
7003
|
-
throw new Error(`asset3d_client_invalid: ${id}`);
|
|
7004
|
-
const applied = applyConfig(client, projectRoot, asset3dLaunchSpec(asset3dHostMode(client, projectRoot)), ASSET3D_SERVER_KEY);
|
|
7005
|
-
process.stdout.write(`${applied.changed ? "UPDATED" : "CURRENT"} ${client.label} Asset3D bridge: ${applied.path}
|
|
7006
|
-
`);
|
|
7007
|
-
if (applied.changed && client.postInstallNote)
|
|
7008
|
-
process.stdout.write(` ${client.postInstallNote}
|
|
7009
|
-
`);
|
|
7010
|
-
}
|
|
7011
|
-
credentialWrite.commit();
|
|
7012
|
-
process.stdout.write(`${result.changed ? "ENABLED" : "CURRENT"} Asset3D provider ${result.providerCommit}; library=${library.library}; service=${access.serviceRoot}; authentication=${access.authentication}; skillFiles=${result.skillFiles}
|
|
7013
|
-
`);
|
|
7014
|
-
return 0;
|
|
7015
|
-
} catch (error) {
|
|
7016
|
-
credentialWrite.rollback();
|
|
7017
|
-
throw error;
|
|
7018
|
-
}
|
|
7019
|
-
}
|
|
7020
|
-
if (operation === "install") {
|
|
7021
|
-
let providerBundle;
|
|
7022
|
-
let expectedSha256;
|
|
7023
|
-
let catalogBaseUrl;
|
|
7024
|
-
let awApiBaseUrl;
|
|
7025
|
-
let awDepotName;
|
|
7026
|
-
let awCredentialFile;
|
|
7027
|
-
const downloadOrigins = [];
|
|
7028
|
-
let clients;
|
|
7029
|
-
let replaceOwned = false;
|
|
7030
|
-
for (let index = 0;index < rest.length; index++) {
|
|
7031
|
-
const arg = rest[index];
|
|
7032
|
-
if (arg === "--replace-owned") {
|
|
7033
|
-
replaceOwned = true;
|
|
7034
|
-
continue;
|
|
7035
|
-
}
|
|
7036
|
-
if (arg === "--provider-bundle" || arg === "--sha256" || arg === "--download-origin" || arg === "--catalog-base-url" || arg === "--aw-base-url" || arg === "--aw-depot-name" || arg === "--aw-credential-file" || arg === "--ide") {
|
|
7037
|
-
const value = rest[++index];
|
|
7038
|
-
if (!value)
|
|
7039
|
-
throw new Error(`${arg} requires a value`);
|
|
7040
|
-
if (arg === "--provider-bundle")
|
|
7041
|
-
providerBundle = value;
|
|
7042
|
-
else if (arg === "--sha256")
|
|
7043
|
-
expectedSha256 = value;
|
|
7044
|
-
else if (arg === "--download-origin")
|
|
7045
|
-
downloadOrigins.push(value);
|
|
7046
|
-
else if (arg === "--catalog-base-url")
|
|
7047
|
-
catalogBaseUrl = value;
|
|
7048
|
-
else if (arg === "--aw-base-url")
|
|
7049
|
-
awApiBaseUrl = normalizeAssetLibraryServiceRoot(value);
|
|
7050
|
-
else if (arg === "--aw-depot-name")
|
|
7051
|
-
awDepotName = parseAssetLibraryId(value);
|
|
7052
|
-
else if (arg === "--aw-credential-file")
|
|
7053
|
-
awCredentialFile = resolve16(value);
|
|
7054
|
-
else
|
|
7055
|
-
clients = canonicalAsset3dClients(value);
|
|
7056
|
-
continue;
|
|
7057
|
-
}
|
|
7058
|
-
throw new Error(`unknown asset3d install option: ${arg}`);
|
|
7059
|
-
}
|
|
7060
|
-
if (!providerBundle || !expectedSha256)
|
|
7061
|
-
throw new Error("usage: forgeax-game asset3d install --provider-bundle <archive> --sha256 <hex> --download-origin <origin> [...]");
|
|
7062
|
-
const result = await installAsset3d({
|
|
7063
|
-
projectRoot,
|
|
7064
|
-
providerBundle,
|
|
7065
|
-
expectedSha256,
|
|
7066
|
-
downloadOrigins,
|
|
7067
|
-
...catalogBaseUrl ? { catalogBaseUrl } : {},
|
|
7068
|
-
...awApiBaseUrl ? { awApiBaseUrl } : {},
|
|
7069
|
-
...awDepotName ? { awDepotName } : {},
|
|
7070
|
-
...awCredentialFile ? { awCredentialFile } : {},
|
|
7071
|
-
...clients ? { clients } : {},
|
|
7072
|
-
replaceOwned
|
|
7073
|
-
});
|
|
7074
|
-
process.stdout.write(`${result.changed ? "INSTALLED" : "CURRENT"} Asset3D provider ${result.providerCommit}; originSetDigest=${result.originSetDigest}; skillFiles=${result.skillFiles}
|
|
5222
|
+
const emit = (ok, value) => process.stdout.write(JSON.stringify({
|
|
5223
|
+
schemaVersion: "1.0.0",
|
|
5224
|
+
command: `${id}.${operation}`,
|
|
5225
|
+
ok,
|
|
5226
|
+
...ok ? { value } : { error: value }
|
|
5227
|
+
}) + `
|
|
7075
5228
|
`);
|
|
7076
|
-
|
|
7077
|
-
|
|
7078
|
-
if (operation === "
|
|
7079
|
-
|
|
7080
|
-
throw new Error("usage: forgeax-game asset3d uninstall");
|
|
7081
|
-
const result = uninstallAsset3d(projectRoot);
|
|
7082
|
-
if (result.modified.length)
|
|
7083
|
-
throw new Error(`owned_skill_modified: ${result.modified.join(", ")}`);
|
|
7084
|
-
process.stdout.write(`REMOVED Asset3D owned records: ${result.removed}
|
|
5229
|
+
try {
|
|
5230
|
+
const extension = discoverExtensions().find((item) => item.id === id);
|
|
5231
|
+
if (operation === "help" || operation === "--help") {
|
|
5232
|
+
process.stdout.write(`${id}: enable [--ide ...] [--local], disable, or a business operation documented in its Skill.
|
|
7085
5233
|
`);
|
|
7086
5234
|
return 0;
|
|
7087
5235
|
}
|
|
7088
|
-
|
|
7089
|
-
|
|
7090
|
-
|
|
7091
|
-
|
|
7092
|
-
|
|
7093
|
-
|
|
7094
|
-
|
|
7095
|
-
|
|
7096
|
-
|
|
7097
|
-
|
|
7098
|
-
|
|
7099
|
-
|
|
7100
|
-
|
|
7101
|
-
|
|
7102
|
-
|
|
7103
|
-
|
|
7104
|
-
|
|
7105
|
-
|
|
7106
|
-
|
|
7107
|
-
|
|
7108
|
-
|
|
7109
|
-
refresh = true;
|
|
7110
|
-
continue;
|
|
7111
|
-
}
|
|
7112
|
-
if (arg === "--provider-result-stdin") {
|
|
7113
|
-
stdin = true;
|
|
7114
|
-
continue;
|
|
7115
|
-
}
|
|
7116
|
-
if (arg === "--execution" && rest[index + 1]) {
|
|
7117
|
-
execution = rest[++index];
|
|
7118
|
-
continue;
|
|
7119
|
-
}
|
|
7120
|
-
throw new Error("usage: forgeax-game asset3d commit --execution <uuid> --json [--provider-result-stdin] [--refresh]");
|
|
5236
|
+
const root = requireProject();
|
|
5237
|
+
if (operation === "enable") {
|
|
5238
|
+
const options = [];
|
|
5239
|
+
let hosts;
|
|
5240
|
+
let local = false;
|
|
5241
|
+
for (let i = 0;i < rest.length; i++) {
|
|
5242
|
+
const arg = rest[i];
|
|
5243
|
+
if (arg === "--local")
|
|
5244
|
+
local = true;
|
|
5245
|
+
else if (arg === "--ide" || arg.startsWith("--ide=")) {
|
|
5246
|
+
const value = arg === "--ide" ? rest[++i] : arg.slice(6);
|
|
5247
|
+
if (!value)
|
|
5248
|
+
throw new Error("extension_host_required");
|
|
5249
|
+
hosts = value.split(",").map((name) => {
|
|
5250
|
+
const client = findClient(name);
|
|
5251
|
+
if (!client)
|
|
5252
|
+
throw new Error("extension_host_invalid: " + name);
|
|
5253
|
+
return client.id;
|
|
5254
|
+
});
|
|
5255
|
+
} else
|
|
5256
|
+
options.push(arg);
|
|
7121
5257
|
}
|
|
7122
|
-
|
|
7123
|
-
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
execution,
|
|
7127
|
-
...stdin ? { providerResult: await readBoundedStdin() } : {},
|
|
7128
|
-
refresh
|
|
7129
|
-
});
|
|
7130
|
-
asset3dEnvelope("asset3d.commit", result.failed === 0, result);
|
|
7131
|
-
return result.failed === 0 ? 0 : 1;
|
|
7132
|
-
}
|
|
7133
|
-
if (operation === "abort") {
|
|
7134
|
-
const executionIndex = rest.indexOf("--execution");
|
|
7135
|
-
const execution = executionIndex >= 0 ? rest[executionIndex + 1] : undefined;
|
|
7136
|
-
if (!execution || rest.some((arg, index) => arg !== "--json" && index !== executionIndex && index !== executionIndex + 1))
|
|
7137
|
-
throw new Error("usage: forgeax-game asset3d abort --execution <uuid> --json");
|
|
7138
|
-
asset3dEnvelope("asset3d.abort", true, abortAsset3d(projectRoot, execution));
|
|
7139
|
-
return 0;
|
|
7140
|
-
}
|
|
7141
|
-
if (operation === "doctor") {
|
|
5258
|
+
const selected = [...new Set(hosts ?? selectClients(root, undefined).selected)];
|
|
5259
|
+
local ||= selected.some((host) => inspectConfig(findClient(host), root, launchSpec("local")).state === "current");
|
|
5260
|
+
emit(true, await enableExtension(root, extension, selected, options, local ? launchSpec("local").args[0] : undefined));
|
|
5261
|
+
} else if (operation === "disable") {
|
|
7142
5262
|
if (rest.some((arg) => arg !== "--json"))
|
|
7143
|
-
throw new Error("
|
|
7144
|
-
|
|
7145
|
-
|
|
5263
|
+
throw new Error("extension_arguments_invalid");
|
|
5264
|
+
emit(true, disableExtension(root, id));
|
|
5265
|
+
} else {
|
|
5266
|
+
const value = await runExtension(root, extension, args);
|
|
5267
|
+
const failed = value && typeof value === "object" && "failed" in value && Number(value.failed) > 0;
|
|
5268
|
+
emit(!failed, value);
|
|
5269
|
+
if (failed)
|
|
5270
|
+
return 1;
|
|
7146
5271
|
}
|
|
7147
|
-
|
|
5272
|
+
return 0;
|
|
7148
5273
|
} catch (error) {
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
return message.startsWith("usage:") ? 2 : 1;
|
|
7153
|
-
}
|
|
7154
|
-
throw error;
|
|
5274
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5275
|
+
emit(false, { code: message.split(":")[0], message: message.slice(0, 256) });
|
|
5276
|
+
return 1;
|
|
7155
5277
|
}
|
|
7156
5278
|
}
|
|
7157
5279
|
async function runCli(argv) {
|
|
@@ -7173,8 +5295,6 @@ async function runCli(argv) {
|
|
|
7173
5295
|
return devkitCommand(args);
|
|
7174
5296
|
case "agents":
|
|
7175
5297
|
return agentsCommand(args);
|
|
7176
|
-
case "asset3d":
|
|
7177
|
-
return asset3dCommand(args);
|
|
7178
5298
|
case "update":
|
|
7179
5299
|
return updateCommand(args);
|
|
7180
5300
|
case "version":
|
|
@@ -7187,6 +5307,8 @@ async function runCli(argv) {
|
|
|
7187
5307
|
process.stdout.write(HELP);
|
|
7188
5308
|
return 0;
|
|
7189
5309
|
default:
|
|
5310
|
+
if (command && discoverExtensions().some((extension) => extension.id === command))
|
|
5311
|
+
return extensionCommand(command, args);
|
|
7190
5312
|
process.stderr.write(`Unknown command: ${command ?? "(none)"}
|
|
7191
5313
|
|
|
7192
5314
|
${HELP}`);
|
|
@@ -7254,7 +5376,7 @@ function parseMcpArgs(args) {
|
|
|
7254
5376
|
} else
|
|
7255
5377
|
throw new Error(`unknown MCP option: ${arg}`);
|
|
7256
5378
|
}
|
|
7257
|
-
return { transport, host, port, root:
|
|
5379
|
+
return { transport, host, port, root: resolve14(root), requireAuth, allowedOrigins };
|
|
7258
5380
|
}
|
|
7259
5381
|
async function runMcp(args) {
|
|
7260
5382
|
const options = parseMcpArgs(args);
|