@forgeax/engine-devkit 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +19 -4
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/__tests__/cli-output.unit.test.d.ts +2 -0
  4. package/dist/__tests__/cli-output.unit.test.d.ts.map +1 -0
  5. package/dist/__tests__/gltf-source-key-diagnostics.integration.test.d.ts +2 -0
  6. package/dist/__tests__/gltf-source-key-diagnostics.integration.test.d.ts.map +1 -0
  7. package/dist/__tests__/preview-port.integration.test.d.ts +2 -0
  8. package/dist/__tests__/preview-port.integration.test.d.ts.map +1 -0
  9. package/dist/__tests__/rhi-debug-build-graph.integration.test.d.ts +2 -0
  10. package/dist/__tests__/rhi-debug-build-graph.integration.test.d.ts.map +1 -0
  11. package/dist/__tests__/sdk-update.unit.test.d.ts +2 -0
  12. package/dist/__tests__/sdk-update.unit.test.d.ts.map +1 -0
  13. package/dist/assets.d.ts.map +1 -1
  14. package/dist/bootstrap-commands.d.ts.map +1 -1
  15. package/dist/cli-output.d.ts +4 -0
  16. package/dist/cli-output.d.ts.map +1 -0
  17. package/dist/cli.mjs +380 -567
  18. package/dist/cli.mjs.map +1 -1
  19. package/dist/commands.d.ts.map +1 -1
  20. package/dist/host/__tests__/bootstrap-root-owner.test-d.d.ts +2 -0
  21. package/dist/host/__tests__/bootstrap-root-owner.test-d.d.ts.map +1 -0
  22. package/dist/host/project-bootstrap.d.ts +4 -2
  23. package/dist/host/project-bootstrap.d.ts.map +1 -1
  24. package/dist/host/resource-bootstrap.d.ts +5 -3
  25. package/dist/host/resource-bootstrap.d.ts.map +1 -1
  26. package/dist/host.d.ts +2 -1
  27. package/dist/host.d.ts.map +1 -1
  28. package/dist/index.d.ts +4 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.mjs +303 -570
  31. package/dist/index.mjs.map +1 -1
  32. package/dist/project.d.ts +0 -1
  33. package/dist/project.d.ts.map +1 -1
  34. package/dist/rhi-debug/cli-context.d.ts.map +1 -1
  35. package/dist/sdk-bootstrap.d.ts +9 -0
  36. package/dist/sdk-bootstrap.d.ts.map +1 -1
  37. package/dist/sdk-cli.mjs +190 -24
  38. package/dist/sdk-cli.mjs.map +1 -1
  39. package/dist/sdk-install.d.ts.map +1 -1
  40. package/dist/sdk-update.d.ts +23 -0
  41. package/dist/sdk-update.d.ts.map +1 -0
  42. package/dist/tools/benchmark/__tests__/benchmark-vocabulary-owner.test-d.d.ts +2 -0
  43. package/dist/tools/benchmark/__tests__/benchmark-vocabulary-owner.test-d.d.ts.map +1 -0
  44. package/dist/tools/benchmark/harness.d.ts.map +1 -1
  45. package/dist/tools/browser-host.d.ts.map +1 -1
  46. package/dist/tools/native-preview.d.ts.map +1 -1
  47. package/dist/types.d.ts +7 -0
  48. package/dist/types.d.ts.map +1 -1
  49. package/package.json +29 -30
  50. package/dist/__tests__/tool-migration.test.d.ts +0 -2
  51. package/dist/__tests__/tool-migration.test.d.ts.map +0 -1
package/dist/cli.mjs CHANGED
@@ -27,7 +27,9 @@ import { execFile } from 'child_process';
27
27
  import { tmpdir } from 'os';
28
28
  import { promisify } from 'util';
29
29
  import { decodeTape, openReplay, buildFrameModel, createRhiDebugError } from '@forgeax/engine-rhi-debug';
30
- import { build, createServer, preview } from 'vite';
30
+ import { rhi, createShaderModule } from '@forgeax/engine-rhi-null';
31
+ import { build, preview, createServer as createServer$1 } from 'vite';
32
+ import { createServer } from 'net';
31
33
  import { startVitest } from 'vitest/node';
32
34
  import materialPreviewPlugin from '@forgeax/engine-preview/material';
33
35
  import meshPreviewPlugin from '@forgeax/engine-preview/mesh';
@@ -299,16 +301,17 @@ var init_dist = __esm({
299
301
  "src/dist.ts"() {
300
302
  }
301
303
  });
304
+
305
+ // src/host.ts
306
+ var host_exports = {};
307
+ __export(host_exports, {
308
+ createViteConfig: () => createViteConfig,
309
+ devKitDdcRoots: () => devKitDdcRoots,
310
+ ignoreDevKitCatalogPath: () => ignoreDevKitCatalogPath
311
+ });
302
312
  function ignoreDevKitCatalogPath(path) {
303
313
  const normalized = path.replace(/\\/g, "/");
304
- if (normalized.split("/").includes("shaders")) return true;
305
- if (!normalized.endsWith(".meta.json")) return false;
306
- try {
307
- const meta = JSON.parse(readFileSync(path, "utf8"));
308
- return meta.importer === "game-default-target-profile";
309
- } catch {
310
- return false;
311
- }
314
+ return normalized.split("/").includes("shaders");
312
315
  }
313
316
  function devKitDdcRoots(projectRoot2) {
314
317
  return {
@@ -574,17 +577,20 @@ await app.pluginContext.plugin({
574
577
  return `import { forgeaxBundlerAdapter } from 'virtual:forgeax/bundler';
575
578
  import { createApp, createToolPreviewHost, createToolPreviewRecipe, fitToolPreviewCameraToAabb, gameHostPlugin, replayToolPreviewCapture } from '@forgeax/engine/app';
576
579
  import { createCatalogSource } from '@forgeax/engine/assets-runtime';
580
+ import {
581
+ createRuntimeAssetImportTransport,
582
+ runtimeBinding,
583
+ } from 'virtual:forgeax/pack-runtime';
577
584
  import { installCatalogLoader, projectPluginEntries } from '@forgeax/engine/plugin/loader';
578
585
  import { audioPlugin } from '@forgeax/engine/audio';
579
586
  import { webAudioPlugin } from '@forgeax/engine/audio-webaudio';
580
587
  import { physicsPlugin } from '@forgeax/engine/physics';
581
588
  import { skinningPlugin } from '@forgeax/engine/skinning';
582
589
  import { createPrimitiveMesh } from '@forgeax/engine/geometry';
583
- import { createDevImportTransport } from '@forgeax/engine/runtime';
584
590
  import { mat4 } from '@forgeax/engine/math';
585
591
  import { CAMERA_PROJECTION_ORTHOGRAPHIC, Camera, DirectionalLight, Materials, MeshFilter, MeshRenderer, Skylight, SkyboxBackground, TONEMAP_NONE, TONEMAP_REINHARD_EXTENDED } from '@forgeax/engine/render';
586
- import { projectSceneAsset, Transform, worldInstantiateScene } from '@forgeax/engine/scene';
587
- import { createStandaloneRuntimeAssetBinding } from '@forgeax/engine/types';
592
+ import { Transform } from '@forgeax/engine/scene';
593
+ import { type SceneAsset } from '@forgeax/engine/types';
588
594
  import { ParticleEffectPlayer, vfxGpuEffectContribution } from '@forgeax/engine/vfx';
589
595
  import { createVfxRuntimeHost } from '@forgeax/engine/vfx-render';
590
596
 
@@ -685,17 +691,39 @@ const resizeCanvas = () => {
685
691
  const resizeObserver = new ResizeObserver(resizeCanvas);
686
692
  resizeObserver.observe(canvas);
687
693
  resizeCanvas();
688
- const runtimeScopeBinding = createStandaloneRuntimeAssetBinding(${JSON.stringify(facts.id)});
694
+ const runtimeScopeBinding = runtimeBinding;
695
+ if (runtimeScopeBinding === undefined) {
696
+ throw new Error('forgeax: Vite Pack runtime binding is required in the generated host');
697
+ }
689
698
  const assetCatalog = createCatalogSource({
690
699
  url: import.meta.env.DEV
691
700
  ? runtimeScopeBinding.catalogUrl
692
701
  : new URL('pack-index.json', document.baseURI).href,
693
- expectedScope: runtimeScopeBinding,
702
+ ...(import.meta.env.DEV ? { expectedScope: runtimeScopeBinding } : {}),
694
703
  });
695
704
  const bundler = {
696
705
  ...forgeaxBundlerAdapter(),
697
- ...(import.meta.env.DEV ? { importTransport: createDevImportTransport(runtimeScopeBinding) } : {}),
706
+ ...(import.meta.env.DEV
707
+ ? { importTransport: createRuntimeAssetImportTransport(runtimeScopeBinding) }
708
+ : {}),
698
709
  };
710
+ const assetPreparation = new WeakMap();
711
+
712
+ function prepareAssetRegistry(assets) {
713
+ const existing = assetPreparation.get(assets);
714
+ if (existing !== undefined) return existing;
715
+ const pending = (async () => {
716
+ assets.configureRuntimeBinding(runtimeScopeBinding);
717
+ assets.setCatalogSource(assetCatalog);
718
+ if (vfxRuntimeHost !== undefined) {
719
+ assets.installDecoder(vfxGpuEffectContribution.kind, vfxGpuEffectContribution.decoder);
720
+ }
721
+ const catalog = await assets.enumerateCatalog();
722
+ if (!catalog.ok) throw catalog.error;
723
+ })();
724
+ assetPreparation.set(assets, pending);
725
+ return pending;
726
+ }
699
727
 
700
728
  function previewStable(value) {
701
729
  if (value instanceof ArrayBuffer) return 'ArrayBuffer:' + JSON.stringify(Array.from(new Uint8Array(value)));
@@ -756,13 +784,14 @@ async function previewOwnerFacts(assets, resource, payload) {
756
784
  }
757
785
 
758
786
  async function prepareProject(app) {
787
+ await prepareAssetRegistry(app.assets);
759
788
  previewWorld = app.world;
760
789
  const assets = app.assets;
761
790
  let canonicalEnvironment;
762
791
  if (resource !== undefined) {
763
792
  if (resource.kind !== 'texture') {
764
793
  if (canonicalEnvironmentGuid === null) throw new Error('canonical preview environment is unavailable');
765
- const environment = await assets.load(canonicalEnvironmentGuid, 'equirect');
794
+ const environment = await assets.loadByGuid(assets.parseGuid(canonicalEnvironmentGuid));
766
795
  if (!environment.ok) throw environment.error;
767
796
  if (environment.value.kind !== 'equirect') {
768
797
  throw new Error(\`canonical preview environment expected equirect, received \${environment.value.kind}\`);
@@ -777,7 +806,7 @@ if (resource !== undefined) {
777
806
  const attached = await vfxRuntimeHost.attachWorld({ world: app.world, assets });
778
807
  if (!attached.ok) throw attached.error;
779
808
  }
780
- const loaded = await assets.load(resource.guid, resource.kind === 'vfx' ? 'particle-effect' : resource.kind);
809
+ const loaded = await assets.loadByGuid(assets.parseGuid(resource.guid));
781
810
  if (!loaded.ok) throw loaded.error;
782
811
  const payload = loaded.value;
783
812
  if (resource.kind === 'vfx' && payload.kind !== 'particle-effect') {
@@ -790,7 +819,7 @@ if (resource !== undefined) {
790
819
  const meshMaterialHandles = resource.kind === 'mesh' && payload.kind === 'mesh'
791
820
  ? await Promise.all(payload.materialSlots.map(async (slot) => {
792
821
  if (slot.defaultMaterial === undefined) return 0;
793
- const material = await assets.load(slot.defaultMaterial, 'material');
822
+ const material = await assets.loadByGuid(assets.parseGuid(slot.defaultMaterial));
794
823
  if (!material.ok) throw material.error;
795
824
  if (material.value.kind !== 'material') {
796
825
  throw new Error('mesh material slot resolved to a non-material asset');
@@ -920,15 +949,13 @@ let defaultScene;
920
949
  let defaultSceneRoot;
921
950
  const defaultSceneGuid = ${JSON.stringify(facts.defaultScene)};
922
951
  if (defaultSceneGuid !== undefined) {
923
- const loaded = await assets.load(defaultSceneGuid, 'scene');
952
+ const loaded = await assets.loadByGuid<SceneAsset>(assets.parseGuid(defaultSceneGuid));
924
953
  if (!loaded.ok) throw loaded.error;
925
- const projected = await projectSceneAsset(app.world, loaded.value, (guid, kind) => assets.load(guid, kind));
926
- if (!projected.ok) throw projected.error;
927
- defaultScene = projected.value;
928
- const handle = app.world.allocSharedRef('SceneAsset', projected.value);
929
- const instantiated = worldInstantiateScene(app.world, handle);
954
+ defaultScene = loaded.value;
955
+ const handle = app.world.allocSharedRef('SceneAsset', loaded.value);
956
+ const instantiated = assets.instantiate<SceneAsset>(handle, app.world);
930
957
  if (!instantiated.ok) throw instantiated.error;
931
- defaultSceneRoot = instantiated.value.root;
958
+ defaultSceneRoot = instantiated.value;
932
959
  }
933
960
  const uiRoot = document.querySelector('#game-ui');
934
961
  await app.pluginContext.plugin(gameHostPlugin({
@@ -967,8 +994,6 @@ if (query.has('forgeax-tool-replay')) {
967
994
  ...(resource === undefined ? {} : { resource }),
968
995
  canvas,
969
996
  app: {
970
- ...(assetCatalog === undefined ? {} : { assetCatalog }),
971
- ...(vfxRuntimeHost === undefined ? {} : { assetDecoders: [vfxGpuEffectContribution] }),
972
997
  plugins: [${plugins.join(", ")}],
973
998
  ...(vfxRuntimeHost === undefined ? {} : { features: [vfxRuntimeHost.feature] }),
974
999
  },
@@ -1081,8 +1106,6 @@ if (query.has('forgeax-tool-replay')) {
1081
1106
  const result = await createApp(
1082
1107
  canvas,
1083
1108
  {
1084
- ...(assetCatalog === undefined ? {} : { assetCatalog }),
1085
- ...(vfxRuntimeHost === undefined ? {} : { assetDecoders: [vfxGpuEffectContribution] }),
1086
1109
  plugins: [${plugins.join(", ")}],
1087
1110
  ...(pointerLockAllowed === undefined ? {} : { pointerLockAllowed }),
1088
1111
  },
@@ -1230,9 +1253,9 @@ async function createViteConfig(facts, command2, base = "/", options = {}) {
1230
1253
  plugins,
1231
1254
  resolve: {
1232
1255
  alias: consumerAliases,
1233
- dedupe: ["@forgeax/engine-app", "@forgeax/engine-ecs", "@forgeax/engine-runtime"]
1256
+ dedupe: ["@forgeax/engine"]
1234
1257
  },
1235
- server: { fs: { allow: [facts.root, ...roots] } },
1258
+ server: { ...options.server, fs: { allow: [facts.root, ...roots] } },
1236
1259
  build: {
1237
1260
  target: "esnext",
1238
1261
  outDir: options.outDir ?? resolve(facts.root, "dist"),
@@ -1256,7 +1279,7 @@ async function readJson(path) {
1256
1279
  function firstUnsupportedStandaloneRealm(entries2, inheritedRealm = "engine") {
1257
1280
  for (const entry of entries2) {
1258
1281
  const realm = entry.realm ?? inheritedRealm ?? "engine";
1259
- if (realm === "host") return { id: entry.id, realm };
1282
+ if (realm !== "engine") return { id: entry.id, realm };
1260
1283
  if (entry.group === true) {
1261
1284
  const unsupported = firstUnsupportedStandaloneRealm(
1262
1285
  entry.config,
@@ -1374,13 +1397,10 @@ async function readProjectFacts(rootInput = process.cwd()) {
1374
1397
  }
1375
1398
  };
1376
1399
  }
1377
- function isCommandError(value) {
1378
- if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
1379
- const candidate = value;
1380
- return typeof candidate.code === "string" && typeof candidate.expected === "string" && typeof candidate.hint === "string" && candidate.detail !== null && typeof candidate.detail === "object" && !Array.isArray(candidate.detail);
1381
- }
1382
1400
  function commandError(cause, fallbackCode) {
1383
- if (isCommandError(cause)) return cause;
1401
+ if (cause !== null && typeof cause === "object" && "code" in cause && "expected" in cause && "hint" in cause && "detail" in cause) {
1402
+ return cause;
1403
+ }
1384
1404
  return {
1385
1405
  code: fallbackCode,
1386
1406
  expected: "the ForgeaX command to complete",
@@ -1392,16 +1412,33 @@ var init_project = __esm({
1392
1412
  "src/project.ts"() {
1393
1413
  }
1394
1414
  });
1395
- function failure(error) {
1396
- return { ok: false, error };
1415
+
1416
+ // src/types.ts
1417
+ function parseProjectPortOption(value, provided) {
1418
+ if (!provided) return { ok: true, value: void 0 };
1419
+ if (value !== void 0 && /^(0|[1-9]\d*)$/.test(value)) {
1420
+ const port = Number(value);
1421
+ if (Number.isSafeInteger(port) && port <= 65535) return { ok: true, value: port };
1422
+ }
1423
+ return {
1424
+ ok: false,
1425
+ error: {
1426
+ code: "cli-parse-error",
1427
+ expected: "--port to be 0 or an integer from 1 to 65535",
1428
+ hint: "Omit --port for strict 5173, pass a positive port for a strict binding, or pass 0 for an OS-assigned port.",
1429
+ detail: { option: "--port", received: value ?? null }
1430
+ }
1431
+ };
1397
1432
  }
1398
- function producerFailure(stderr, fallback, detail) {
1399
- try {
1400
- const parsed = JSON.parse(stderr.at(-1) ?? "");
1401
- if (isCommandError(parsed)) return failure(parsed);
1402
- } catch {
1433
+ function resolveProjectPort(port) {
1434
+ return { port: port ?? 5173, strictPort: port !== 0 };
1435
+ }
1436
+ var init_types = __esm({
1437
+ "src/types.ts"() {
1403
1438
  }
1404
- return failure({ ...fallback, detail: { ...detail, diagnostic: stderr.join("\n") } });
1439
+ });
1440
+ function failure(error) {
1441
+ return { ok: false, error };
1405
1442
  }
1406
1443
  async function sourcesAt(path) {
1407
1444
  const info = await stat(path);
@@ -1486,15 +1523,16 @@ async function addGltf(sourcePath, dryRun2) {
1486
1523
  stderrWrite: (line) => stderr.push(line)
1487
1524
  });
1488
1525
  if (exitCode !== 0) {
1489
- return producerFailure(
1490
- stderr,
1491
- {
1526
+ try {
1527
+ return failure(JSON.parse(stderr.at(-1) ?? ""));
1528
+ } catch {
1529
+ return failure({
1492
1530
  code: "asset-add-failed",
1493
1531
  expected: "the glTF producer to create or reuse a valid sidecar",
1494
- hint: "Inspect the glTF source and its external references."
1495
- },
1496
- { source: sourcePath }
1497
- );
1532
+ hint: "Inspect the glTF source and its external references.",
1533
+ detail: { source: sourcePath, diagnostic: stderr.join("\n") }
1534
+ });
1535
+ }
1498
1536
  }
1499
1537
  return { ok: true, value: { source: sourcePath, metaPath: `${sourcePath}.meta.json` } };
1500
1538
  }
@@ -1537,15 +1575,16 @@ async function entries(options) {
1537
1575
  { stdoutWrite: (line) => stdout.push(line), stderrWrite: (line) => stderr.push(line) }
1538
1576
  );
1539
1577
  if (!result.ok) {
1540
- return producerFailure(
1541
- stderr,
1542
- {
1578
+ try {
1579
+ return failure(JSON.parse(stderr.at(-1) ?? ""));
1580
+ } catch {
1581
+ return failure({
1543
1582
  code: "asset-authority-invalid",
1544
1583
  expected: "all asset roots and sidecars to pass the pack scanner",
1545
- hint: "Repair the first invalid asset authority reported by the scanner."
1546
- },
1547
- {}
1548
- );
1584
+ hint: "Repair the first invalid asset authority reported by the scanner.",
1585
+ detail: { diagnostic: stderr.join("\n") }
1586
+ });
1587
+ }
1549
1588
  }
1550
1589
  return { ok: true, value: { facts: facts.value, entries: result.value } };
1551
1590
  }
@@ -1630,7 +1669,6 @@ var init_package = __esm({
1630
1669
  "@forgeax/engine-audio-webaudio": "workspace:*",
1631
1670
  "@forgeax/engine-fbx": "workspace:*",
1632
1671
  "@forgeax/engine-font": "workspace:*",
1633
- "@forgeax/engine-geometry": "workspace:*",
1634
1672
  "@forgeax/engine-gltf": "workspace:*",
1635
1673
  "@forgeax/engine-image": "workspace:*",
1636
1674
  "@forgeax/engine-input": "workspace:*",
@@ -1640,6 +1678,7 @@ var init_package = __esm({
1640
1678
  "@forgeax/engine-preview": "workspace:*",
1641
1679
  "@forgeax/engine-project": "workspace:*",
1642
1680
  "@forgeax/engine-rhi-debug": "workspace:*",
1681
+ "@forgeax/engine-rhi-null": "workspace:*",
1643
1682
  "@forgeax/engine-render": "workspace:*",
1644
1683
  "@forgeax/engine-render-graph": "workspace:*",
1645
1684
  "@forgeax/engine-runtime": "workspace:*",
@@ -1657,7 +1696,6 @@ var init_package = __esm({
1657
1696
  vitest: "4.1.5"
1658
1697
  },
1659
1698
  devDependencies: {
1660
- "@forgeax/engine-rhi-null": "workspace:*",
1661
1699
  "@types/node": "^20.14.0"
1662
1700
  },
1663
1701
  forgeax: {
@@ -1871,6 +1909,20 @@ var init_sdk = __esm({
1871
1909
  "src/sdk.ts"() {
1872
1910
  }
1873
1911
  });
1912
+ function agentOnboarding(sdk, projectRoot2) {
1913
+ const root = projectRoot2 ?? sdk.root;
1914
+ return {
1915
+ read: [
1916
+ resolve(root, "AGENTS.md"),
1917
+ resolve(root, "skills", "forgeax-engine-sdk", "SKILL.md"),
1918
+ resolve(root, "skills", "forgeax-engine-sdk", "references", "feature-catalog.md")
1919
+ ],
1920
+ next: projectRoot2 === void 0 ? {
1921
+ cwd: root,
1922
+ argv: ["node", "./bin/forgeax.mjs", "new", "../my-game"]
1923
+ } : { cwd: root, argv: ["pnpm", "exec", "forgeax", "list", "--json"] }
1924
+ };
1925
+ }
1874
1926
  function sdkInitPath(sdk) {
1875
1927
  return resolve(sdk.root, ...SDK_INIT_PATH);
1876
1928
  }
@@ -1885,7 +1937,7 @@ function sdkProjectInstallArgs(store) {
1885
1937
  "--side-effects-cache=true",
1886
1938
  "--child-concurrency=1"
1887
1939
  ];
1888
- return store === void 0 ? common : [...common, "--offline", "--trust-lockfile", "--store-dir", store];
1940
+ return store === void 0 ? common : [...common, "--offline", "--config.trust-lockfile=true", "--store-dir", store];
1889
1941
  }
1890
1942
  function sdkBootstrapInstallArgs(store) {
1891
1943
  const common = [
@@ -1894,7 +1946,7 @@ function sdkBootstrapInstallArgs(store) {
1894
1946
  "--child-concurrency=1",
1895
1947
  "--side-effects-cache=true"
1896
1948
  ];
1897
- return store === void 0 ? common : [...common, "--offline", "--trust-lockfile", "--store-dir", store];
1949
+ return store === void 0 ? common : [...common, "--offline", "--config.trust-lockfile=true", "--store-dir", store];
1898
1950
  }
1899
1951
  function supportedPnpm(version) {
1900
1952
  const [major = 0, minor = 0] = version.split(".").map(Number);
@@ -1984,7 +2036,8 @@ async function sdkInitCommand(sdk, options = {}) {
1984
2036
  const report = {
1985
2037
  ...state,
1986
2038
  root: sdk.root,
1987
- store: sdk.store === void 0 ? "registry" : "offline"
2039
+ store: sdk.store === void 0 ? "registry" : "offline",
2040
+ onboarding: agentOnboarding(sdk)
1988
2041
  };
1989
2042
  if (options.dryRun === true || options.install === false) return { ok: true, value: report };
1990
2043
  let staging;
@@ -2019,6 +2072,107 @@ var init_sdk_bootstrap = __esm({
2019
2072
  SDK_INIT_SCHEMA_VERSION = "1.0.0";
2020
2073
  }
2021
2074
  });
2075
+
2076
+ // src/sdk-update.ts
2077
+ function parseVersion(version) {
2078
+ const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(
2079
+ version
2080
+ );
2081
+ if (match === null) return void 0;
2082
+ const major = match[1];
2083
+ const minor = match[2];
2084
+ const patch = match[3];
2085
+ if (major === void 0 || minor === void 0 || patch === void 0) return void 0;
2086
+ const prerelease = match[4]?.split(".");
2087
+ if (prerelease?.some(
2088
+ (identifier) => identifier.length === 0 || /^\d+$/.test(identifier) && /^0\d+/.test(identifier)
2089
+ )) {
2090
+ return void 0;
2091
+ }
2092
+ return {
2093
+ core: [major, minor, patch],
2094
+ ...prerelease === void 0 ? {} : { prerelease }
2095
+ };
2096
+ }
2097
+ function compareNumericIdentifier(current, latest) {
2098
+ if (current.length !== latest.length) return latest.length > current.length ? 1 : -1;
2099
+ if (current === latest) return 0;
2100
+ return latest > current ? 1 : -1;
2101
+ }
2102
+ function comparePrerelease(current, latest) {
2103
+ const length = Math.max(current.length, latest.length);
2104
+ for (let index = 0; index < length; index += 1) {
2105
+ const currentIdentifier = current[index];
2106
+ const latestIdentifier = latest[index];
2107
+ if (currentIdentifier === void 0) return 1;
2108
+ if (latestIdentifier === void 0) return -1;
2109
+ if (currentIdentifier === latestIdentifier) continue;
2110
+ const currentNumeric = /^\d+$/.test(currentIdentifier);
2111
+ const latestNumeric = /^\d+$/.test(latestIdentifier);
2112
+ if (currentNumeric && latestNumeric) {
2113
+ return compareNumericIdentifier(currentIdentifier, latestIdentifier);
2114
+ }
2115
+ if (currentNumeric !== latestNumeric) return latestNumeric ? -1 : 1;
2116
+ return latestIdentifier > currentIdentifier ? 1 : -1;
2117
+ }
2118
+ return 0;
2119
+ }
2120
+ function newerSdkVersion(currentVersion, latestVersion) {
2121
+ const current = parseVersion(currentVersion);
2122
+ const latest = parseVersion(latestVersion);
2123
+ if (current === void 0 || latest === void 0) return false;
2124
+ for (let index = 0; index < current.core.length; index += 1) {
2125
+ const precedence = compareNumericIdentifier(
2126
+ current.core[index] ?? "0",
2127
+ latest.core[index] ?? "0"
2128
+ );
2129
+ if (precedence !== 0) return precedence > 0;
2130
+ }
2131
+ if (current.prerelease !== void 0 && latest.prerelease === void 0) return true;
2132
+ if (current.prerelease === void 0 || latest.prerelease === void 0) return false;
2133
+ return comparePrerelease(current.prerelease, latest.prerelease) > 0;
2134
+ }
2135
+ function offline() {
2136
+ return ["1", "true"].includes((process.env.npm_config_offline ?? "").toLowerCase());
2137
+ }
2138
+ async function checkSdkUpdate(currentVersion) {
2139
+ if (process.env.FORGEAX_DISABLE_UPDATE_CHECK === "1") {
2140
+ return { status: "skipped", currentVersion, reason: "disabled" };
2141
+ }
2142
+ if (offline()) return { status: "skipped", currentVersion, reason: "offline" };
2143
+ try {
2144
+ const registry = process.env.npm_config_registry ?? "https://registry.npmjs.org/";
2145
+ const response = await fetch(
2146
+ new URL("@forgeax%2Fengine-sdk", `${registry.replace(/\/$/, "")}/`),
2147
+ {
2148
+ headers: { accept: "application/vnd.npm.install-v1+json" },
2149
+ signal: AbortSignal.timeout(2e3)
2150
+ }
2151
+ );
2152
+ if (!response.ok) throw new Error(`registry-http-${response.status}`);
2153
+ const metadata = await response.json();
2154
+ const latestVersion = metadata["dist-tags"]?.latest;
2155
+ if (typeof latestVersion !== "string") throw new Error("registry-latest-version-missing");
2156
+ return newerSdkVersion(currentVersion, latestVersion) ? {
2157
+ status: "available",
2158
+ currentVersion,
2159
+ latestVersion,
2160
+ migrationRisk: UPDATE_MIGRATION_RISK
2161
+ } : { status: "current", currentVersion, latestVersion };
2162
+ } catch (cause) {
2163
+ return {
2164
+ status: "unavailable",
2165
+ currentVersion,
2166
+ reason: cause instanceof Error ? cause.message : String(cause)
2167
+ };
2168
+ }
2169
+ }
2170
+ var UPDATE_MIGRATION_RISK;
2171
+ var init_sdk_update = __esm({
2172
+ "src/sdk-update.ts"() {
2173
+ UPDATE_MIGRATION_RISK = "Install a newer SDK in a separate directory. Existing games remain pinned and are not migrated automatically; review release notes and migrate and test each game before changing its Engine version.";
2174
+ }
2175
+ });
2022
2176
  function slash(path) {
2023
2177
  return path.split(sep).join("/");
2024
2178
  }
@@ -2646,9 +2800,16 @@ async function newCommand(options = {}) {
2646
2800
  env: { ...process.env, CI: "true" },
2647
2801
  maxBuffer: 16 * 1024 * 1024
2648
2802
  });
2803
+ const sdkUpdate = await checkSdkUpdate(sdk.manifest.sdkVersion);
2649
2804
  return {
2650
2805
  ok: true,
2651
- value: { root, template: templateId, sdkVersion: sdk.manifest.sdkVersion }
2806
+ value: {
2807
+ root,
2808
+ template: templateId,
2809
+ sdkVersion: sdk.manifest.sdkVersion,
2810
+ onboarding: agentOnboarding(sdk, root),
2811
+ sdkUpdate
2812
+ }
2652
2813
  };
2653
2814
  } catch (cause) {
2654
2815
  if (staging !== void 0) await rm(staging, { recursive: true, force: true });
@@ -2681,6 +2842,7 @@ var init_bootstrap_commands = __esm({
2681
2842
  init_project();
2682
2843
  init_sdk();
2683
2844
  init_sdk_bootstrap();
2845
+ init_sdk_update();
2684
2846
  init_skill_install();
2685
2847
  execFileAsync2 = promisify(execFile);
2686
2848
  }
@@ -2849,491 +3011,12 @@ var init_plugin_authoring = __esm({
2849
3011
  execFileAsync3 = promisify(execFile);
2850
3012
  }
2851
3013
  });
2852
- var RhiError;
2853
- var init_dist2 = __esm({
2854
- "../rhi/dist/index.mjs"() {
2855
- RhiError = class extends Error {
2856
- code;
2857
- expected;
2858
- hint;
2859
- detail;
2860
- constructor(args2) {
2861
- super(`[RhiError ${args2.code}] expected: ${args2.expected}; hint: ${args2.hint}`);
2862
- this.name = "RhiError";
2863
- this.code = args2.code;
2864
- this.expected = args2.expected;
2865
- this.hint = args2.hint;
2866
- this.detail = args2.detail;
2867
- }
2868
- };
2869
- }
2870
- });
2871
- function readPassLabel(desc) {
2872
- if (desc && typeof desc.label === "string" && desc.label.length > 0) {
2873
- return desc.label;
2874
- }
2875
- return "<unnamed>";
2876
- }
2877
- function readRecord(handle) {
2878
- if (handle === null || typeof handle !== "object") return void 0;
2879
- const tagged = handle;
2880
- return tagged[BOOKKEEPING_KEY];
2881
- }
2882
- function isHandleDestroyed(handle) {
2883
- if (readRecord(handle)?.destroyed === true) {
2884
- return err(
2885
- new RhiError({
2886
- code: "destroy-after-destroy",
2887
- expected: "GPU buffer/texture handle has not been destroyed yet",
2888
- hint: "object already destroyed; track lifecycle in caller or check isDestroyed before re-destroy"
2889
- })
2890
- );
2891
- }
2892
- return ok(void 0);
2893
- }
2894
- function acquireCanvasContext(_canvas) {
2895
- return ok(new RhiNullCanvasContext());
2896
- }
2897
- function createShaderModule(_device, _desc) {
2898
- return Promise.resolve(ok({}));
2899
- }
2900
- function requestAdapter(_opts, _compatibleSurface) {
2901
- return Promise.resolve(ok(new RhiNullAdapter()));
2902
- }
2903
- var RhiNullRenderPassEncoder, RhiNullComputePassEncoder, DeviceCounter, RhiNullCommandEncoder, BOOKKEEPING_KEY, Bookkeeper, nextDeviceId, RhiNullDevice, EMPTY_FEATURES, EMPTY_LIMITS, NEVER, RhiNullQueue, RhiNullAdapter, RhiNullCanvasContext, rhi;
2904
- var init_dist3 = __esm({
2905
- "../rhi-null/dist/index.mjs"() {
2906
- init_dist2();
2907
- RhiNullRenderPassEncoder = class {
2908
- /** Number of draw* calls issued on this pass (AC-06 readback). */
2909
- drawCount = 0;
2910
- bindGroupCount = 0;
2911
- /** Most recent setVertexBuffer / setBindGroup ownership validation; ok unless
2912
- * a foreign handle was passed (AC-09 readback). */
2913
- lastValidation = ok(void 0);
2914
- bookkeeper;
2915
- counter;
2916
- passName;
2917
- constructor(bookkeeper, counter, passName) {
2918
- this.bookkeeper = bookkeeper;
2919
- this.counter = counter;
2920
- this.passName = passName;
2921
- }
2922
- setPipeline(_pipeline) {
2923
- }
2924
- setVertexBuffer(_slot, buffer, _offset, _size) {
2925
- this.lastValidation = this.bookkeeper.validateOwnership(buffer);
2926
- }
2927
- setIndexBuffer(_buffer, _format, _offset, _size) {
2928
- }
2929
- setBindGroup(_index, bindGroup, _dynamicOffsetsData, _dynamicOffsetsDataStart, _dynamicOffsetsDataLength) {
2930
- this.bindGroupCount++;
2931
- this.counter?.recordBindGroup();
2932
- this.lastValidation = this.bookkeeper.validateOwnership(bindGroup);
2933
- }
2934
- draw(_vertexCount, _instanceCount, _firstVertex, _firstInstance) {
2935
- this.drawCount++;
2936
- this.counter?.recordDraw();
2937
- }
2938
- drawIndexed(_indexCount, _instanceCount, _firstIndex, _baseVertex, _firstInstance) {
2939
- this.drawCount++;
2940
- this.counter?.recordDraw();
2941
- }
2942
- end() {
2943
- this.counter?.recordPassName(this.passName);
2944
- }
2945
- setViewport(_x, _y, _w, _h, _minDepth, _maxDepth) {
2946
- }
2947
- setScissorRect(_x, _y, _w, _h) {
2948
- }
2949
- setBlendConstant(_color) {
2950
- }
2951
- setStencilReference(_reference) {
2952
- }
2953
- drawIndirect(_indirectBuffer, _indirectOffset) {
2954
- this.drawCount++;
2955
- this.counter?.recordDraw();
2956
- }
2957
- drawIndexedIndirect(_indirectBuffer, _indirectOffset) {
2958
- this.drawCount++;
2959
- this.counter?.recordDraw();
2960
- }
2961
- pushDebugGroup(_groupLabel) {
2962
- }
2963
- popDebugGroup() {
2964
- }
2965
- insertDebugMarker(_markerLabel) {
2966
- }
2967
- executeBundles(_bundles) {
2968
- return ok(void 0);
2969
- }
2970
- beginOcclusionQuery(_queryIndex) {
2971
- return ok(void 0);
2972
- }
2973
- endOcclusionQuery() {
2974
- return ok(void 0);
2975
- }
2976
- };
2977
- RhiNullComputePassEncoder = class {
2978
- /** Number of dispatchWorkgroups calls issued on this pass (readback). */
2979
- dispatchCount = 0;
2980
- /** Most recent setBindGroup ownership validation (AC-09 readback). */
2981
- lastValidation = ok(void 0);
2982
- bookkeeper;
2983
- counter;
2984
- passName;
2985
- constructor(bookkeeper, counter, passName) {
2986
- this.bookkeeper = bookkeeper;
2987
- this.counter = counter;
2988
- this.passName = passName;
2989
- }
2990
- setPipeline(_pipeline) {
2991
- }
2992
- setBindGroup(_index, bindGroup, _dynamicOffsets) {
2993
- this.lastValidation = this.bookkeeper.validateOwnership(bindGroup);
2994
- }
2995
- dispatchWorkgroups(_x, _y, _z) {
2996
- this.dispatchCount++;
2997
- this.counter?.recordDispatch();
2998
- }
2999
- dispatchWorkgroupsIndirect(indirectBuffer, _indirectOffset) {
3000
- this.lastValidation = this.bookkeeper.validateOwnership(indirectBuffer);
3001
- this.dispatchCount++;
3002
- this.counter?.recordDispatch();
3003
- }
3004
- end() {
3005
- this.counter?.recordPassName(this.passName);
3006
- }
3007
- };
3008
- DeviceCounter = class {
3009
- constructor(device) {
3010
- this.device = device;
3011
- }
3012
- device;
3013
- recordDraw() {
3014
- this.device.totalDrawCount++;
3015
- }
3016
- recordDispatch() {
3017
- this.device.totalDispatchCount++;
3018
- }
3019
- recordBindGroup() {
3020
- this.device.totalBindGroupCount++;
3021
- }
3022
- recordPassName(name) {
3023
- this.device.framePassNames.push(name);
3024
- }
3025
- };
3026
- RhiNullCommandEncoder = class {
3027
- bookkeeper;
3028
- counter;
3029
- constructor(bookkeeper, device) {
3030
- this.bookkeeper = bookkeeper;
3031
- this.counter = new DeviceCounter(device);
3032
- }
3033
- beginRenderPass(desc) {
3034
- const label = readPassLabel(desc);
3035
- return new RhiNullRenderPassEncoder(this.bookkeeper, this.counter, label);
3036
- }
3037
- beginComputePass(desc) {
3038
- const label = readPassLabel(desc);
3039
- return new RhiNullComputePassEncoder(this.bookkeeper, this.counter, label);
3040
- }
3041
- copyBufferToBuffer(_source, _sourceOffsetOrDestination, _destinationOrSize, _destinationOffset, _size) {
3042
- }
3043
- copyBufferToTexture(_source, _destination, _copySize) {
3044
- }
3045
- copyTextureToBuffer(_source, _destination, _copySize) {
3046
- }
3047
- copyTextureToTexture(_source, _destination, _copySize) {
3048
- }
3049
- clearBuffer(_buffer, _offset, _size) {
3050
- }
3051
- resolveQuerySet(_querySet, _firstQuery, _queryCount, _destination, _destinationOffset) {
3052
- return ok(void 0);
3053
- }
3054
- writeTimestamp(_querySet, _queryIndex) {
3055
- }
3056
- pushDebugGroup(_groupLabel) {
3057
- }
3058
- popDebugGroup() {
3059
- }
3060
- insertDebugMarker(_markerLabel) {
3061
- }
3062
- finish() {
3063
- return ok(this.bookkeeper.register("CommandBuffer"));
3064
- }
3065
- };
3066
- BOOKKEEPING_KEY = /* @__PURE__ */ Symbol("forgeax-rhi-null-bookkeeping");
3067
- Bookkeeper = class {
3068
- deviceId;
3069
- nextHandleId = 0;
3070
- records = /* @__PURE__ */ new Map();
3071
- constructor(deviceId) {
3072
- this.deviceId = deviceId;
3073
- }
3074
- /**
3075
- * Register a freshly-minted handle of the given kind, returning a plain
3076
- * object that carries its ledger row. The caller casts the return value to
3077
- * the concrete brand (`as unknown as Buffer` etc.).
3078
- */
3079
- register(kind) {
3080
- const id = this.nextHandleId++;
3081
- const record = {
3082
- id,
3083
- kind,
3084
- destroyed: false,
3085
- sourceDeviceId: this.deviceId
3086
- };
3087
- this.records.set(id, record);
3088
- return { [BOOKKEEPING_KEY]: record };
3089
- }
3090
- /**
3091
- * Mark a handle destroyed. Fail-fasts on a second destroy
3092
- * ('destroy-after-destroy') or on a handle issued by a different device
3093
- * ('rhi-not-available'); otherwise flips the destroyed flag and returns ok.
3094
- */
3095
- destroy(handle) {
3096
- const ownership = this.validateOwnership(handle);
3097
- if (!ownership.ok) return ownership;
3098
- const destroyedCheck = isHandleDestroyed(handle);
3099
- if (!destroyedCheck.ok) return destroyedCheck;
3100
- ownership.value.destroyed = true;
3101
- return ok(void 0);
3102
- }
3103
- /**
3104
- * Report whether a handle has already been destroyed (true once destroy()
3105
- * has flipped its flag). Used by command-stream methods that must not
3106
- * consume a stale handle.
3107
- */
3108
- isDestroyed(handle) {
3109
- return readRecord(handle)?.destroyed === true;
3110
- }
3111
- /**
3112
- * Validate that a handle was issued by THIS device. Returns the ledger row on
3113
- * success so callers can mutate it (e.g. flip destroyed); returns
3114
- * 'rhi-not-available' for a foreign handle (AC-09 — no silent pass).
3115
- */
3116
- validateOwnership(handle) {
3117
- const record = readRecord(handle);
3118
- if (record === void 0 || record.sourceDeviceId !== this.deviceId) {
3119
- return err(
3120
- new RhiError({
3121
- code: "rhi-not-available",
3122
- expected: "handle was issued by this RhiNull device",
3123
- hint: "do not pass a handle created on a different RhiNull device into this device; create resources on the device they are used with"
3124
- })
3125
- );
3126
- }
3127
- return ok(record);
3128
- }
3129
- /**
3130
- * Return all ledger rows as a readonly array. M3 unit tests (w17) read this
3131
- * to assert create/destroy pairing, BGL/PSO shape counts, and resource
3132
- * lifecycle coverage (AC-05/06/07). Returns a snapshot of the current Map
3133
- * so callers can iterate without a stale reference after further mutations.
3134
- */
3135
- allRecords() {
3136
- return [...this.records.values()];
3137
- }
3138
- /** Report the total number of records in the ledger. */
3139
- recordCount() {
3140
- return this.records.size;
3141
- }
3142
- };
3143
- nextDeviceId = 0;
3144
- RhiNullDevice = class {
3145
- internalBookkeeper;
3146
- nullQueue;
3147
- encoderFactory;
3148
- /** Per-frame total draw count across all pass encoders executed this frame
3149
- * (aggregated by the command encoder on finish, then reset). M3 unit tests
3150
- * (w17) read this to assert draw count >= 1 (AC-06). */
3151
- totalDrawCount = 0;
3152
- /** Per-frame total direct and indirect compute dispatch count. */
3153
- totalDispatchCount = 0;
3154
- /** Per-frame total bind group set count (AC-06 / AC-05 readback). */
3155
- totalBindGroupCount = 0;
3156
- /** Per-frame pass names executed this frame, in schedule order (AC-04). */
3157
- framePassNames = [];
3158
- /** The per-device handle ledger — exposed so M3 tests can assert create/destroy
3159
- * pairing and BGL/PSO shape counts (AC-05/06/07). */
3160
- get bookkeeper() {
3161
- return this.internalBookkeeper;
3162
- }
3163
- constructor(queue, encoderFactory) {
3164
- this.internalBookkeeper = new Bookkeeper(nextDeviceId++);
3165
- this.nullQueue = queue;
3166
- this.encoderFactory = encoderFactory;
3167
- }
3168
- get caps() {
3169
- return {
3170
- backendKind: "null",
3171
- compute: true,
3172
- timestampQuery: false,
3173
- timestampPeriodNanoseconds: null,
3174
- indirectDrawing: true,
3175
- textureCompressionBc: false,
3176
- textureCompressionEtc2: false,
3177
- textureCompressionAstc: false,
3178
- // 3 wgpu-native-only reserved flags stay false on non-native backends
3179
- // (D-5); the headless backend is not a native runtime.
3180
- multiDrawIndirect: false,
3181
- pushConstants: false,
3182
- textureBindingArray: false,
3183
- samplerAliasing: true,
3184
- firstInstanceIndirect: true,
3185
- storageBuffer: true,
3186
- storageTexture: true,
3187
- rgba16floatRenderable: true,
3188
- rg11b10ufloatRenderable: true,
3189
- float32Filterable: true,
3190
- maxColorAttachments: 8
3191
- };
3192
- }
3193
- get features() {
3194
- return EMPTY_FEATURES;
3195
- }
3196
- get limits() {
3197
- return EMPTY_LIMITS;
3198
- }
3199
- get queue() {
3200
- return this.nullQueue;
3201
- }
3202
- // forgeax-async-whitelist: dom-native — spec `GPUDevice.lost` Promise
3203
- // passthrough. The headless backend never loses a device (no GPU), so the
3204
- // Promise stays unsettled for the lifetime of the device, mirroring a live
3205
- // device that never transitions to the lost state.
3206
- get lost() {
3207
- return NEVER;
3208
- }
3209
- createBuffer(_desc) {
3210
- return ok(this.internalBookkeeper.register("Buffer"));
3211
- }
3212
- createTexture(_desc) {
3213
- return ok(this.internalBookkeeper.register("Texture"));
3214
- }
3215
- destroyBuffer(buf) {
3216
- return this.internalBookkeeper.destroy(buf);
3217
- }
3218
- destroyQuerySet(querySet) {
3219
- return this.internalBookkeeper.destroy(querySet);
3220
- }
3221
- destroyTexture(tex) {
3222
- return this.internalBookkeeper.destroy(tex);
3223
- }
3224
- createTextureView(_texture, _desc) {
3225
- return ok(this.internalBookkeeper.register("TextureView"));
3226
- }
3227
- createSampler(_desc) {
3228
- return ok(this.internalBookkeeper.register("Sampler"));
3229
- }
3230
- createBindGroupLayout(_desc) {
3231
- return ok(this.internalBookkeeper.register("BindGroupLayout"));
3232
- }
3233
- createBindGroup(_desc) {
3234
- return ok(this.internalBookkeeper.register("BindGroup"));
3235
- }
3236
- createPipelineLayout(_desc) {
3237
- return ok(this.internalBookkeeper.register("PipelineLayout"));
3238
- }
3239
- createRenderPipeline(_desc) {
3240
- return ok(this.makePipeline("RenderPipeline"));
3241
- }
3242
- createComputePipeline(_desc) {
3243
- return ok(this.makePipeline("ComputePipeline"));
3244
- }
3245
- createQuerySet(desc) {
3246
- if (desc.type === "timestamp") {
3247
- return err(
3248
- new RhiError({
3249
- code: "feature-not-enabled",
3250
- expected: "caps.timestampQuery === true (timestamp-query feature)",
3251
- hint: "RhiNull is structural-only and cannot produce GPU timestamp ticks"
3252
- })
3253
- );
3254
- }
3255
- return ok(this.internalBookkeeper.register("QuerySet"));
3256
- }
3257
- createCommandEncoder(_desc) {
3258
- return ok(this.encoderFactory(this.internalBookkeeper, this));
3259
- }
3260
- /**
3261
- * Mint a pipeline handle whose object also carries the no-op
3262
- * `getBindGroupLayout(index)` ops method (D-2). The prod auto-layout path
3263
- * (debug-draw.ts) and the existing mock unit tests both call
3264
- * `pipeline.getBindGroupLayout(n)`; returning a legal BindGroupLayout brand
3265
- * (recorded in the ledger) keeps those consumers from crashing on a missing
3266
- * method.
3267
- */
3268
- makePipeline(kind) {
3269
- const handle = this.internalBookkeeper.register(kind);
3270
- const getBindGroupLayout = (_index) => this.internalBookkeeper.register("BindGroupLayout");
3271
- return Object.assign(handle, { getBindGroupLayout });
3272
- }
3273
- };
3274
- EMPTY_FEATURES = /* @__PURE__ */ new Set();
3275
- EMPTY_LIMITS = {};
3276
- NEVER = new Promise(() => {
3277
- });
3278
- RhiNullQueue = class {
3279
- writeBuffer(_buffer, _bufferOffset, _data, _dataOffset, _size) {
3280
- return ok(void 0);
3281
- }
3282
- writeTexture(_destination, _data, _dataLayout, _size) {
3283
- return ok(void 0);
3284
- }
3285
- copyExternalImageToTexture(_source, _destination, _copySize) {
3286
- return ok(void 0);
3287
- }
3288
- submit(_commandBuffers) {
3289
- return ok(void 0);
3290
- }
3291
- // forgeax-async-whitelist: dom-native — spec `GPUQueue.onSubmittedWorkDone`
3292
- // never rejects. The headless backend has no pending GPU work, so it resolves
3293
- // immediately (AC-12: read-back idioms must not hang).
3294
- onSubmittedWorkDone() {
3295
- return Promise.resolve(void 0);
3296
- }
3297
- };
3298
- RhiNullAdapter = class {
3299
- features = /* @__PURE__ */ new Set();
3300
- limits = {};
3301
- // forgeax-async-whitelist is not needed: this returns Promise<Result<...>>
3302
- // per the spec contract; never rejects.
3303
- requestDevice(_opts) {
3304
- const device = new RhiNullDevice(
3305
- new RhiNullQueue(),
3306
- (bookkeeper, dev) => new RhiNullCommandEncoder(bookkeeper, dev)
3307
- );
3308
- return Promise.resolve(ok(device));
3309
- }
3310
- };
3311
- RhiNullCanvasContext = class {
3312
- configure(_desc) {
3313
- return ok(void 0);
3314
- }
3315
- unconfigure() {
3316
- }
3317
- getConfiguration() {
3318
- return void 0;
3319
- }
3320
- getCurrentTexture() {
3321
- return ok({});
3322
- }
3323
- };
3324
- rhi = {
3325
- requestAdapter,
3326
- acquireCanvasContext,
3327
- createShaderModule
3328
- };
3329
- }
3330
- });
3331
3014
  function createCliRhiDebugOperationContext() {
3332
3015
  return {
3333
3016
  captureFrame: async () => err(
3334
3017
  createRhiDebugError("capture-unavailable", {
3335
3018
  stage: "capture",
3336
- cause: "the standalone CLI has no live App capture provider"
3019
+ cause: "the standalone CLI has no live App capture provider; start a recorder-enabled live host and invoke its rhiCapture root"
3337
3020
  })
3338
3021
  ),
3339
3022
  async readArtifact(artifact) {
@@ -3396,7 +3079,6 @@ function createCliRhiDebugOperationContext() {
3396
3079
  }
3397
3080
  var init_cli_context = __esm({
3398
3081
  "src/rhi-debug/cli-context.ts"() {
3399
- init_dist3();
3400
3082
  }
3401
3083
  });
3402
3084
  function createRhiDebugOperationContext(host) {
@@ -3734,7 +3416,15 @@ async function sdkInstallCommand(options) {
3734
3416
  await rename(resolve(staging, name), resolve(root, name));
3735
3417
  }
3736
3418
  }
3737
- return { ok: true, value: { root, sdkVersion: version, source: "@forgeax/engine-sdk" } };
3419
+ return {
3420
+ ok: true,
3421
+ value: {
3422
+ root,
3423
+ sdkVersion: version,
3424
+ source: "@forgeax/engine-sdk",
3425
+ next: { cwd: root, argv: ["node", "./bin/forgeax.mjs", "init"] }
3426
+ }
3427
+ };
3738
3428
  } catch (cause) {
3739
3429
  return { ok: false, error: commandError(cause, "sdk-install-failed") };
3740
3430
  } finally {
@@ -3817,6 +3507,21 @@ __export(commands_exports, {
3817
3507
  function runRhiDebugCommand(name, input, context) {
3818
3508
  return runRhiDebugOperation(name, input, context);
3819
3509
  }
3510
+ async function materializeViteDevPort(port) {
3511
+ if (port !== 0) return port;
3512
+ const reservation = createServer();
3513
+ await new Promise((resolveListen, rejectListen) => {
3514
+ reservation.once("error", rejectListen);
3515
+ reservation.listen(0, "127.0.0.1", resolveListen);
3516
+ });
3517
+ const address = reservation.address();
3518
+ const assigned = typeof address === "object" && address !== null ? address.port : 0;
3519
+ await new Promise((resolveClose, rejectClose) => {
3520
+ reservation.close((error) => error === void 0 ? resolveClose() : rejectClose(error));
3521
+ });
3522
+ if (assigned === 0) throw new Error("OS did not assign an ephemeral preview port");
3523
+ return assigned;
3524
+ }
3820
3525
  async function buildCommand(options = {}) {
3821
3526
  const facts = await readProjectFacts(options.root);
3822
3527
  if (!facts.ok) return facts;
@@ -3919,13 +3624,38 @@ async function devCommand(options = {}) {
3919
3624
  const facts = await readProjectFacts(options.root);
3920
3625
  if (!facts.ok) return facts;
3921
3626
  const previous = process.cwd();
3627
+ let server;
3922
3628
  try {
3629
+ const { createServer: createServer2 } = await import('vite');
3630
+ const { createViteConfig: createViteConfig2 } = await Promise.resolve().then(() => (init_host(), host_exports));
3631
+ const port = resolveProjectPort(options.port);
3632
+ const vitePort = { ...port, port: await materializeViteDevPort(port.port) };
3923
3633
  process.chdir(facts.value.root);
3924
- const server = await createServer(await createViteConfig(facts.value, "serve"));
3925
- await server.listen();
3634
+ server = await createServer2(
3635
+ await createViteConfig2(facts.value, "serve", "/", {
3636
+ server: vitePort
3637
+ })
3638
+ );
3639
+ await server.listen(vitePort.port);
3926
3640
  if (options.json !== true) server.printUrls();
3927
- return { ok: true, value: { root: facts.value.root, urls: server.resolvedUrls } };
3641
+ return {
3642
+ ok: true,
3643
+ value: {
3644
+ root: facts.value.root,
3645
+ urls: server.resolvedUrls,
3646
+ mode: "dev",
3647
+ serves: "source",
3648
+ capabilities: {
3649
+ "rhi.capture": {
3650
+ available: false,
3651
+ realm: "host",
3652
+ reason: "standalone-dev-server-has-no-live-app-cli-attachment"
3653
+ }
3654
+ }
3655
+ }
3656
+ };
3928
3657
  } catch (cause) {
3658
+ await server?.close();
3929
3659
  process.chdir(previous);
3930
3660
  return { ok: false, error: commandError(cause, "dev-server-failed") };
3931
3661
  }
@@ -3935,15 +3665,31 @@ async function previewCommand(options = {}) {
3935
3665
  const verified = await verifyDist(resolve(root, "dist"));
3936
3666
  if (!verified.ok) return verified;
3937
3667
  try {
3668
+ const port = resolveProjectPort(options.port);
3938
3669
  const server = await preview({
3939
3670
  root,
3940
3671
  configFile: false,
3941
3672
  base: verified.value.base,
3942
- preview: { open: false, port: 0, strictPort: false },
3673
+ preview: { open: false, ...port },
3943
3674
  build: { outDir: resolve(root, "dist") }
3944
3675
  });
3945
3676
  if (options.json !== true) server.printUrls();
3946
- return { ok: true, value: { root, urls: server.resolvedUrls } };
3677
+ return {
3678
+ ok: true,
3679
+ value: {
3680
+ root,
3681
+ urls: server.resolvedUrls,
3682
+ mode: "preview",
3683
+ serves: "dist",
3684
+ capabilities: {
3685
+ "rhi.capture": {
3686
+ available: false,
3687
+ realm: "host",
3688
+ reason: "static-dist-host-does-not-install-dev-capture"
3689
+ }
3690
+ }
3691
+ }
3692
+ };
3947
3693
  } catch (cause) {
3948
3694
  return { ok: false, error: commandError(cause, "preview-server-failed") };
3949
3695
  }
@@ -3993,6 +3739,7 @@ var init_commands = __esm({
3993
3739
  init_dist();
3994
3740
  init_host();
3995
3741
  init_project();
3742
+ init_types();
3996
3743
  init_assets();
3997
3744
  init_bootstrap_commands();
3998
3745
  init_plugin_authoring();
@@ -4365,7 +4112,7 @@ function hasStaticToolDeclarations(entry) {
4365
4112
  return Array.isArray(Reflect.get(entry.config, "tools"));
4366
4113
  }
4367
4114
  async function createViteModuleLoader(root) {
4368
- const server = await createServer({
4115
+ const server = await createServer$1({
4369
4116
  root,
4370
4117
  appType: "custom",
4371
4118
  configFile: false,
@@ -4622,6 +4369,36 @@ var init_runtime = __esm({
4622
4369
  }
4623
4370
  });
4624
4371
 
4372
+ // src/cli-output.ts
4373
+ function agentOnboardingLines(value) {
4374
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return [];
4375
+ const onboarding = value.onboarding;
4376
+ if (onboarding === null || typeof onboarding !== "object" || Array.isArray(onboarding)) return [];
4377
+ const candidate = onboarding;
4378
+ const lines = Array.isArray(candidate.read) ? candidate.read.filter((path) => typeof path === "string").map((path) => `[forgeax] read: ${path}`) : [];
4379
+ if (typeof candidate.next?.cwd === "string" && Array.isArray(candidate.next.argv) && candidate.next.argv.every((part) => typeof part === "string")) {
4380
+ lines.push(`[forgeax] next cwd: ${candidate.next.cwd}`);
4381
+ lines.push(`[forgeax] next: ${candidate.next.argv.join(" ")}`);
4382
+ }
4383
+ return lines;
4384
+ }
4385
+ function sdkUpdateLines(value) {
4386
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return [];
4387
+ const update = value.sdkUpdate;
4388
+ if (update === null || typeof update !== "object" || Array.isArray(update)) return [];
4389
+ const candidate = update;
4390
+ if (candidate.status !== "available" || typeof candidate.currentVersion !== "string" || typeof candidate.latestVersion !== "string" || typeof candidate.migrationRisk !== "string") {
4391
+ return [];
4392
+ }
4393
+ return [
4394
+ `[forgeax] update available: @forgeax/engine-sdk ${candidate.currentVersion} -> ${candidate.latestVersion}`,
4395
+ `[forgeax] update note: ${candidate.migrationRisk}`
4396
+ ];
4397
+ }
4398
+ function renderForgeaxUsage() {
4399
+ return "Usage: forgeax new [directory] [--template empty|game-default]\n forgeax <init|doctor|test|dev|build|package|serve|preview> [directory]\n forgeax package [directory] [--output release/game-web.zip]\n forgeax run <rhi.capture|rhi.summary|rhi.inspect> [options]\n forgeax run <operation-id> --input <request.json>\n forgeax asset <add|verify|inspect|list> [subject]\n forgeax shader check [path]\n forgeax plugin <install|uninstall> <module-or-id> [options]\n forgeax skill <install|verify> [--root directory]\n forgeax sdk install <directory> [--version VERSION]\n";
4400
+ }
4401
+
4625
4402
  // src/cli.ts
4626
4403
  init_commands();
4627
4404
 
@@ -4821,13 +4598,16 @@ async function execCommand(options) {
4821
4598
  }
4822
4599
 
4823
4600
  // src/cli.ts
4824
- var args = process.argv.slice(2);
4601
+ init_types();
4602
+ var rawArgs = process.argv.slice(2);
4603
+ var args = [...rawArgs];
4825
4604
  var primary = args.shift();
4826
4605
  var nested = primary === "asset" || primary === "shader" || primary === "plugin" || primary === "skill" || primary === "sdk" ? args.shift() : void 0;
4827
4606
  var command = primary === "asset" || primary === "shader" || primary === "plugin" || primary === "skill" || primary === "sdk" ? `${primary}.${nested ?? ""}` : primary;
4828
4607
  var json = args.includes("--json");
4829
4608
  var dryRun = args.includes("--dry-run");
4830
4609
  var noInstall = args.includes("--no-install");
4610
+ var help = rawArgs.includes("--help") || rawArgs.includes("-h");
4831
4611
  function option(name) {
4832
4612
  const index = args.indexOf(name);
4833
4613
  return index < 0 ? void 0 : args[index + 1];
@@ -4844,6 +4624,7 @@ var optionValueIndexes = new Set(
4844
4624
  "--input",
4845
4625
  "--output",
4846
4626
  "--root",
4627
+ "--port",
4847
4628
  "--template",
4848
4629
  "--version",
4849
4630
  "--work-index"
@@ -4882,6 +4663,18 @@ var commands = [
4882
4663
  "describe",
4883
4664
  "exec"
4884
4665
  ];
4666
+ function projectServerOptions() {
4667
+ const parsedPort = parseProjectPortOption(option("--port"), args.includes("--port"));
4668
+ if (!parsedPort.ok) return parsedPort;
4669
+ return {
4670
+ ok: true,
4671
+ value: {
4672
+ root: projectRoot,
4673
+ json,
4674
+ ...parsedPort.value === void 0 ? {} : { port: parsedPort.value }
4675
+ }
4676
+ };
4677
+ }
4885
4678
  async function run(value) {
4886
4679
  switch (value) {
4887
4680
  case "new": {
@@ -4898,8 +4691,10 @@ async function run(value) {
4898
4691
  return doctorCommand({ root: projectRoot});
4899
4692
  case "test":
4900
4693
  return testCommand({ root: projectRoot});
4901
- case "dev":
4902
- return devCommand({ root: projectRoot, json });
4694
+ case "dev": {
4695
+ const options = projectServerOptions();
4696
+ return options.ok ? devCommand(options.value) : options;
4697
+ }
4903
4698
  case "build": {
4904
4699
  const base = option("--base");
4905
4700
  const outDir = option("--out-dir");
@@ -4917,10 +4712,14 @@ async function run(value) {
4917
4712
  ...output === void 0 ? {} : { output }
4918
4713
  });
4919
4714
  }
4920
- case "preview":
4921
- return previewCommand({ root: projectRoot, json });
4922
- case "serve":
4923
- return devCommand({ root: projectRoot, json });
4715
+ case "preview": {
4716
+ const options = projectServerOptions();
4717
+ return options.ok ? previewCommand(options.value) : options;
4718
+ }
4719
+ case "serve": {
4720
+ const options = projectServerOptions();
4721
+ return options.ok ? devCommand(options.value) : options;
4722
+ }
4924
4723
  case "plugin.install": {
4925
4724
  const module = positionals[0];
4926
4725
  const id = option("--id");
@@ -5097,10 +4896,10 @@ async function run(value) {
5097
4896
  }
5098
4897
  }
5099
4898
  }
5100
- if (command === void 0 || !commands.includes(command)) {
5101
- process.stderr.write(
5102
- "Usage: forgeax new [directory] [--template empty|game-default]\n forgeax <init|doctor|test|dev|build|package|serve|preview> [directory]\n forgeax package [directory] [--output release/game-web.zip]\n forgeax run <rhi.capture|rhi.summary|rhi.inspect> [options]\n forgeax run <operation-id> --input <request.json>\n forgeax asset <add|verify|inspect|list> [subject]\n forgeax shader check [path]\n forgeax plugin <install|uninstall> <module-or-id> [options]\n forgeax skill <install|verify> [--root directory]\n forgeax sdk install <directory> [--version VERSION]\n"
5103
- );
4899
+ if (help && primary !== "run") {
4900
+ process.stdout.write(renderForgeaxUsage());
4901
+ } else if (command === void 0 || !commands.includes(command)) {
4902
+ process.stderr.write(renderForgeaxUsage());
5104
4903
  process.exitCode = 2;
5105
4904
  } else {
5106
4905
  const stdoutWrite = process.stdout.write;
@@ -5120,9 +4919,23 @@ if (command === void 0 || !commands.includes(command)) {
5120
4919
  const envelope = result.ok ? { schemaVersion: "1.0.0", command, ok: true, value: result.value } : { schemaVersion: "1.0.0", command, ok: false, error: result.error };
5121
4920
  if (json) stdoutWrite.call(process.stdout, `${JSON.stringify(envelope)}
5122
4921
  `);
5123
- else if (result.ok) process.stdout.write(`[forgeax] ${command} ready
4922
+ else if (result.ok) {
4923
+ process.stdout.write(`[forgeax] ${command} ready
4924
+ `);
4925
+ for (const line of agentOnboardingLines(result.value)) process.stdout.write(`${line}
4926
+ `);
4927
+ for (const line of sdkUpdateLines(result.value)) process.stdout.write(`${line}
4928
+ `);
4929
+ if (command === "sdk.install") {
4930
+ const value = result.value;
4931
+ if (typeof value.next?.cwd === "string" && Array.isArray(value.next.argv) && value.next.argv.every((part) => typeof part === "string")) {
4932
+ process.stdout.write(`[forgeax] next cwd: ${value.next.cwd}
5124
4933
  `);
5125
- else process.stderr.write(`[forgeax] ${result.error.code}: ${result.error.hint}
4934
+ process.stdout.write(`[forgeax] next: ${value.next.argv.join(" ")}
4935
+ `);
4936
+ }
4937
+ }
4938
+ } else process.stderr.write(`[forgeax] ${result.error.code}: ${result.error.hint}
5126
4939
  `);
5127
4940
  if (!result.ok) process.exitCode = result.error.code === "cli-parse-error" ? 2 : 1;
5128
4941
  }