@forgeax/game 0.3.7 → 0.3.9

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