@forgeax/engine-devkit 0.1.3 → 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 (53) hide show
  1. package/README.md +30 -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 +728 -641
  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 +662 -655
  31. package/dist/index.mjs.map +1 -1
  32. package/dist/init.d.ts +2 -1
  33. package/dist/init.d.ts.map +1 -1
  34. package/dist/project.d.ts +0 -1
  35. package/dist/project.d.ts.map +1 -1
  36. package/dist/rhi-debug/cli-context.d.ts.map +1 -1
  37. package/dist/sdk-bootstrap.d.ts +34 -0
  38. package/dist/sdk-bootstrap.d.ts.map +1 -0
  39. package/dist/sdk-cli.mjs +412 -66
  40. package/dist/sdk-cli.mjs.map +1 -1
  41. package/dist/sdk-install.d.ts.map +1 -1
  42. package/dist/sdk-update.d.ts +23 -0
  43. package/dist/sdk-update.d.ts.map +1 -0
  44. package/dist/tools/benchmark/__tests__/benchmark-vocabulary-owner.test-d.d.ts +2 -0
  45. package/dist/tools/benchmark/__tests__/benchmark-vocabulary-owner.test-d.d.ts.map +1 -0
  46. package/dist/tools/benchmark/harness.d.ts.map +1 -1
  47. package/dist/tools/browser-host.d.ts.map +1 -1
  48. package/dist/tools/native-preview.d.ts.map +1 -1
  49. package/dist/types.d.ts +7 -0
  50. package/dist/types.d.ts.map +1 -1
  51. package/package.json +29 -30
  52. package/dist/__tests__/tool-migration.test.d.ts +0 -2
  53. package/dist/__tests__/tool-migration.test.d.ts.map +0 -1
package/dist/cli.mjs CHANGED
@@ -11,7 +11,10 @@ import { fbxImporter } from '@forgeax/engine-fbx';
11
11
  import { fontImporter } from '@forgeax/engine-font/font-importer';
12
12
  import { gltfImporter } from '@forgeax/engine-gltf';
13
13
  import { imageImporter } from '@forgeax/engine-image/image-importer';
14
+ import { BUILTIN_MESH_ASSETS } from '@forgeax/engine-pack/builtin';
15
+ import { scanInventory } from '@forgeax/engine-pack/scanner';
14
16
  import { describeResourcePreviewFailure, validateCanonicalKitReceipt, createMaterialPreviewContribution, createMeshPreviewContribution, createVfxPreviewContribution, createTexturePreviewContribution, validatePreviewArtifactManifest } from '@forgeax/engine-preview';
17
+ import { createMaterialPackCooker } from '@forgeax/engine-shader-compiler';
15
18
  import { createStandaloneRuntimeAssetBinding, ok, err } from '@forgeax/engine-types';
16
19
  import { createParticleCodeNativeCookerFromRoots } from '@forgeax/engine-vfx-compiler';
17
20
  import { pluginPack, reloadAssetHost } from '@forgeax/engine-vite-plugin-pack';
@@ -21,10 +24,12 @@ import { runCliGltf } from '@forgeax/engine-gltf/cli-gltf';
21
24
  import { scanEntries } from '@forgeax/engine-pack/cli-asset';
22
25
  import { AssetGuid } from '@forgeax/engine-pack/guid';
23
26
  import { execFile } from 'child_process';
27
+ import { tmpdir } from 'os';
24
28
  import { promisify } from 'util';
25
29
  import { decodeTape, openReplay, buildFrameModel, createRhiDebugError } from '@forgeax/engine-rhi-debug';
26
- import { tmpdir } from 'os';
27
- 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';
28
33
  import { startVitest } from 'vitest/node';
29
34
  import materialPreviewPlugin from '@forgeax/engine-preview/material';
30
35
  import meshPreviewPlugin from '@forgeax/engine-preview/mesh';
@@ -296,16 +301,17 @@ var init_dist = __esm({
296
301
  "src/dist.ts"() {
297
302
  }
298
303
  });
304
+
305
+ // src/host.ts
306
+ var host_exports = {};
307
+ __export(host_exports, {
308
+ createViteConfig: () => createViteConfig,
309
+ devKitDdcRoots: () => devKitDdcRoots,
310
+ ignoreDevKitCatalogPath: () => ignoreDevKitCatalogPath
311
+ });
299
312
  function ignoreDevKitCatalogPath(path) {
300
313
  const normalized = path.replace(/\\/g, "/");
301
- if (normalized.split("/").includes("shaders")) return true;
302
- if (!normalized.endsWith(".meta.json")) return false;
303
- try {
304
- const meta = JSON.parse(readFileSync(path, "utf8"));
305
- return meta.importer === "game-default-target-profile";
306
- } catch {
307
- return false;
308
- }
314
+ return normalized.split("/").includes("shaders");
309
315
  }
310
316
  function devKitDdcRoots(projectRoot2) {
311
317
  return {
@@ -319,6 +325,42 @@ function isResourcePreviewIgnoredPath(path) {
319
325
  function isProjectSourceIgnoredPath(path) {
320
326
  return ignoreDevKitCatalogPath(path) || path.endsWith(".wgsl.meta.json");
321
327
  }
328
+ async function prepareBuiltinPack(projectRoots, generated, ignorePath) {
329
+ const inventory = await scanInventory(projectRoots, { ignorePath });
330
+ if (!inventory.ok) return void 0;
331
+ const declared = /* @__PURE__ */ new Set();
332
+ for (const declaration of inventory.value.declarations.values()) {
333
+ if (declaration.format === "pack.json") {
334
+ for (const asset of declaration.value.assets) declared.add(asset.guid.toLowerCase());
335
+ continue;
336
+ }
337
+ for (const asset of declaration.value.subAssets) declared.add(asset.guid.toLowerCase());
338
+ }
339
+ const missing = BUILTIN_MESH_ASSETS.filter((asset) => !declared.has(asset.guid.toLowerCase()));
340
+ if (missing.length === 0) return void 0;
341
+ const packPath = resolve(generated, "engine-builtins.pack.json");
342
+ await writeFile(
343
+ packPath,
344
+ `${JSON.stringify(
345
+ {
346
+ schemaVersion: "2.0.0",
347
+ kind: "internal-text-package",
348
+ assets: missing.map((asset) => ({
349
+ guid: asset.guid,
350
+ kind: "mesh",
351
+ payload: { geometry: asset.geometry },
352
+ refs: [],
353
+ artifacts: {}
354
+ }))
355
+ },
356
+ null,
357
+ 2
358
+ )}
359
+ `,
360
+ "utf8"
361
+ );
362
+ return packPath;
363
+ }
322
364
  function findEngineWorkspaceRoot() {
323
365
  let cursor = dirname(fileURLToPath(import.meta.url));
324
366
  for (; ; ) {
@@ -334,6 +376,7 @@ async function engineWorkspacePackages() {
334
376
  const workspaceRoot = findEngineWorkspaceRoot();
335
377
  if (workspaceRoot === void 0) return /* @__PURE__ */ new Map();
336
378
  const packageRoot = resolve(workspaceRoot, "packages");
379
+ if (!existsSync(packageRoot)) return /* @__PURE__ */ new Map();
337
380
  const packages = /* @__PURE__ */ new Map();
338
381
  for (const entry of await readdir(packageRoot, { withFileTypes: true })) {
339
382
  if (!entry.isDirectory()) continue;
@@ -428,6 +471,18 @@ async function createEngineWorkspaceResolver() {
428
471
  }
429
472
  };
430
473
  }
474
+ async function consumerEngineAliases(projectRoot2) {
475
+ const root = resolve(projectRoot2, "node_modules", ".pnpm", "node_modules", "@forgeax");
476
+ try {
477
+ const entries2 = await readdir(root, { withFileTypes: true });
478
+ return entries2.filter((entry) => entry.isDirectory() && entry.name.startsWith("engine-")).map((entry) => ({
479
+ find: `@forgeax/${entry.name}`,
480
+ replacement: resolve(root, entry.name)
481
+ })).sort((a, b) => a.find.localeCompare(b.find));
482
+ } catch {
483
+ return [];
484
+ }
485
+ }
431
486
  async function resolveCanonicalKit() {
432
487
  const packageJson = hostRequire.resolve("@forgeax/engine-preview/package.json");
433
488
  const root = resolve(packageJson, "..", "assets/canonical-kit");
@@ -502,6 +557,7 @@ function hostSource(facts, bootstrapRoot, canonicalEnvironmentGuid) {
502
557
  const plugins = bootstrapRoot === "resource-bootstrap" ? [] : [
503
558
  `webAudioPlugin()`,
504
559
  `audioPlugin()`,
560
+ `skinningPlugin()`,
505
561
  ...facts.physics === void 0 ? [] : [`physicsPlugin('${facts.physics === "2d" ? "rapier-2d" : "rapier-3d"}')`]
506
562
  ];
507
563
  const bootstrapPlugin = facts.bootstrapEntry === void 0 ? "" : `const bootstrapModule = await import(${JSON.stringify(
@@ -519,20 +575,24 @@ await app.pluginContext.plugin({
519
575
  },
520
576
  });`;
521
577
  return `import { forgeaxBundlerAdapter } from 'virtual:forgeax/bundler';
522
- import { createApp, createToolPreviewHost, createToolPreviewRecipe, fitToolPreviewCameraToAabb, gameHostPlugin, replayToolPreviewCapture } from '@forgeax/engine-app';
523
- import { createCatalogSource } from '@forgeax/engine-assets-runtime';
524
- import { installCatalogLoader, projectPluginEntries } from '@forgeax/engine-plugin/loader';
525
- import { audioPlugin } from '@forgeax/engine-audio';
526
- import { webAudioPlugin } from '@forgeax/engine-audio-webaudio';
527
- import { physicsPlugin } from '@forgeax/engine-physics';
528
- import { createPrimitiveMesh } from '@forgeax/engine-geometry';
529
- import { createDevImportTransport } from '@forgeax/engine-runtime';
530
- import { mat4 } from '@forgeax/engine-math';
531
- import { CAMERA_PROJECTION_ORTHOGRAPHIC, Camera, DirectionalLight, Materials, MeshFilter, MeshRenderer, Skylight, SkyboxBackground, TONEMAP_NONE, TONEMAP_REINHARD_EXTENDED } from '@forgeax/engine-render';
532
- import { projectSceneAsset, Transform, worldInstantiateScene } from '@forgeax/engine-scene';
533
- import { createStandaloneRuntimeAssetBinding } from '@forgeax/engine-types';
534
- import { ParticleEffectPlayer, vfxGpuEffectContribution } from '@forgeax/engine-vfx';
535
- import { createVfxRuntimeHost } from '@forgeax/engine-vfx-render';
578
+ import { createApp, createToolPreviewHost, createToolPreviewRecipe, fitToolPreviewCameraToAabb, gameHostPlugin, replayToolPreviewCapture } from '@forgeax/engine/app';
579
+ import { createCatalogSource } from '@forgeax/engine/assets-runtime';
580
+ import {
581
+ createRuntimeAssetImportTransport,
582
+ runtimeBinding,
583
+ } from 'virtual:forgeax/pack-runtime';
584
+ import { installCatalogLoader, projectPluginEntries } from '@forgeax/engine/plugin/loader';
585
+ import { audioPlugin } from '@forgeax/engine/audio';
586
+ import { webAudioPlugin } from '@forgeax/engine/audio-webaudio';
587
+ import { physicsPlugin } from '@forgeax/engine/physics';
588
+ import { skinningPlugin } from '@forgeax/engine/skinning';
589
+ import { createPrimitiveMesh } from '@forgeax/engine/geometry';
590
+ import { mat4 } from '@forgeax/engine/math';
591
+ import { CAMERA_PROJECTION_ORTHOGRAPHIC, Camera, DirectionalLight, Materials, MeshFilter, MeshRenderer, Skylight, SkyboxBackground, TONEMAP_NONE, TONEMAP_REINHARD_EXTENDED } from '@forgeax/engine/render';
592
+ import { Transform } from '@forgeax/engine/scene';
593
+ import { type SceneAsset } from '@forgeax/engine/types';
594
+ import { ParticleEffectPlayer, vfxGpuEffectContribution } from '@forgeax/engine/vfx';
595
+ import { createVfxRuntimeHost } from '@forgeax/engine/vfx-render';
536
596
 
537
597
  const pluginCatalog = new Map([
538
598
  ${catalogModules(facts).map(
@@ -631,16 +691,39 @@ const resizeCanvas = () => {
631
691
  const resizeObserver = new ResizeObserver(resizeCanvas);
632
692
  resizeObserver.observe(canvas);
633
693
  resizeCanvas();
634
- const binding = import.meta.env.DEV
635
- ? createStandaloneRuntimeAssetBinding(${JSON.stringify(facts.id)})
636
- : undefined;
637
- const assetCatalog = binding === undefined
638
- ? undefined
639
- : createCatalogSource({ url: binding.catalogUrl, expectedScope: binding });
694
+ const runtimeScopeBinding = runtimeBinding;
695
+ if (runtimeScopeBinding === undefined) {
696
+ throw new Error('forgeax: Vite Pack runtime binding is required in the generated host');
697
+ }
698
+ const assetCatalog = createCatalogSource({
699
+ url: import.meta.env.DEV
700
+ ? runtimeScopeBinding.catalogUrl
701
+ : new URL('pack-index.json', document.baseURI).href,
702
+ ...(import.meta.env.DEV ? { expectedScope: runtimeScopeBinding } : {}),
703
+ });
640
704
  const bundler = {
641
705
  ...forgeaxBundlerAdapter(),
642
- ...(binding === undefined ? {} : { importTransport: createDevImportTransport(binding) }),
706
+ ...(import.meta.env.DEV
707
+ ? { importTransport: createRuntimeAssetImportTransport(runtimeScopeBinding) }
708
+ : {}),
643
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
+ }
644
727
 
645
728
  function previewStable(value) {
646
729
  if (value instanceof ArrayBuffer) return 'ArrayBuffer:' + JSON.stringify(Array.from(new Uint8Array(value)));
@@ -701,13 +784,14 @@ async function previewOwnerFacts(assets, resource, payload) {
701
784
  }
702
785
 
703
786
  async function prepareProject(app) {
787
+ await prepareAssetRegistry(app.assets);
704
788
  previewWorld = app.world;
705
789
  const assets = app.assets;
706
790
  let canonicalEnvironment;
707
791
  if (resource !== undefined) {
708
792
  if (resource.kind !== 'texture') {
709
793
  if (canonicalEnvironmentGuid === null) throw new Error('canonical preview environment is unavailable');
710
- const environment = await assets.load(canonicalEnvironmentGuid, 'equirect');
794
+ const environment = await assets.loadByGuid(assets.parseGuid(canonicalEnvironmentGuid));
711
795
  if (!environment.ok) throw environment.error;
712
796
  if (environment.value.kind !== 'equirect') {
713
797
  throw new Error(\`canonical preview environment expected equirect, received \${environment.value.kind}\`);
@@ -722,7 +806,7 @@ if (resource !== undefined) {
722
806
  const attached = await vfxRuntimeHost.attachWorld({ world: app.world, assets });
723
807
  if (!attached.ok) throw attached.error;
724
808
  }
725
- const loaded = await assets.load(resource.guid, resource.kind === 'vfx' ? 'particle-effect' : resource.kind);
809
+ const loaded = await assets.loadByGuid(assets.parseGuid(resource.guid));
726
810
  if (!loaded.ok) throw loaded.error;
727
811
  const payload = loaded.value;
728
812
  if (resource.kind === 'vfx' && payload.kind !== 'particle-effect') {
@@ -735,7 +819,7 @@ if (resource !== undefined) {
735
819
  const meshMaterialHandles = resource.kind === 'mesh' && payload.kind === 'mesh'
736
820
  ? await Promise.all(payload.materialSlots.map(async (slot) => {
737
821
  if (slot.defaultMaterial === undefined) return 0;
738
- const material = await assets.load(slot.defaultMaterial, 'material');
822
+ const material = await assets.loadByGuid(assets.parseGuid(slot.defaultMaterial));
739
823
  if (!material.ok) throw material.error;
740
824
  if (material.value.kind !== 'material') {
741
825
  throw new Error('mesh material slot resolved to a non-material asset');
@@ -865,15 +949,13 @@ let defaultScene;
865
949
  let defaultSceneRoot;
866
950
  const defaultSceneGuid = ${JSON.stringify(facts.defaultScene)};
867
951
  if (defaultSceneGuid !== undefined) {
868
- const loaded = await assets.load(defaultSceneGuid, 'scene');
952
+ const loaded = await assets.loadByGuid<SceneAsset>(assets.parseGuid(defaultSceneGuid));
869
953
  if (!loaded.ok) throw loaded.error;
870
- const projected = await projectSceneAsset(app.world, loaded.value, (guid, kind) => assets.load(guid, kind));
871
- if (!projected.ok) throw projected.error;
872
- defaultScene = projected.value;
873
- const handle = app.world.allocSharedRef('SceneAsset', projected.value);
874
- const instantiated = worldInstantiateScene(app.world, handle);
954
+ defaultScene = loaded.value;
955
+ const handle = app.world.allocSharedRef('SceneAsset', loaded.value);
956
+ const instantiated = assets.instantiate<SceneAsset>(handle, app.world);
875
957
  if (!instantiated.ok) throw instantiated.error;
876
- defaultSceneRoot = instantiated.value.root;
958
+ defaultSceneRoot = instantiated.value;
877
959
  }
878
960
  const uiRoot = document.querySelector('#game-ui');
879
961
  await app.pluginContext.plugin(gameHostPlugin({
@@ -912,8 +994,6 @@ if (query.has('forgeax-tool-replay')) {
912
994
  ...(resource === undefined ? {} : { resource }),
913
995
  canvas,
914
996
  app: {
915
- ...(assetCatalog === undefined ? {} : { assetCatalog }),
916
- ...(vfxRuntimeHost === undefined ? {} : { assetDecoders: [vfxGpuEffectContribution] }),
917
997
  plugins: [${plugins.join(", ")}],
918
998
  ...(vfxRuntimeHost === undefined ? {} : { features: [vfxRuntimeHost.feature] }),
919
999
  },
@@ -1018,12 +1098,16 @@ if (query.has('forgeax-tool-replay')) {
1018
1098
  window.addEventListener('pagehide', () => void host.value.dispose(), { once: true });
1019
1099
  }
1020
1100
  } else {
1101
+ // WebDriver/headless browsers cannot satisfy the trusted-gesture requirement
1102
+ // for pointer lock. Keep the game running and let its keyboard fallback own
1103
+ // camera input; real browser users retain the normal click-to-lock path.
1104
+ const pointerLockAllowed =
1105
+ typeof navigator !== 'undefined' && navigator.webdriver === true ? () => false : undefined;
1021
1106
  const result = await createApp(
1022
1107
  canvas,
1023
1108
  {
1024
- ...(assetCatalog === undefined ? {} : { assetCatalog }),
1025
- ...(vfxRuntimeHost === undefined ? {} : { assetDecoders: [vfxGpuEffectContribution] }),
1026
1109
  plugins: [${plugins.join(", ")}],
1110
+ ...(pointerLockAllowed === undefined ? {} : { pointerLockAllowed }),
1027
1111
  },
1028
1112
  bundler,
1029
1113
  );
@@ -1082,10 +1166,32 @@ function htmlSource(title) {
1082
1166
  <div id="app-shell"><canvas id="app"></canvas><div id="game-ui"></div></div><div id="forgeax-fatal" role="alert"></div>
1083
1167
  <script>
1084
1168
  (() => {
1169
+ const formatStartupFailure = (reason) => {
1170
+ if (reason instanceof Error) return reason.message;
1171
+ if (reason !== null && typeof reason === 'object') {
1172
+ const record = reason;
1173
+ const lines = [];
1174
+ for (const key of ['code', 'expected', 'hint']) {
1175
+ if (typeof record[key] === 'string' && record[key].length > 0) {
1176
+ lines.push(key + ': ' + record[key]);
1177
+ }
1178
+ }
1179
+ const detail = record.detail;
1180
+ if (detail !== null && typeof detail === 'object') {
1181
+ for (const key of ['reason', 'guid']) {
1182
+ if (typeof detail[key] === 'string' && detail[key].length > 0) {
1183
+ lines.push('detail.' + key + ': ' + detail[key]);
1184
+ }
1185
+ }
1186
+ }
1187
+ if (lines.length > 0) return lines.join('\\n');
1188
+ }
1189
+ return String(reason ?? 'Unknown startup failure');
1190
+ };
1085
1191
  const show = (reason) => {
1086
1192
  const notice = document.querySelector('#forgeax-fatal');
1087
1193
  if (!(notice instanceof HTMLElement)) return;
1088
- const message = reason instanceof Error ? reason.message : String(reason ?? 'Unknown startup failure');
1194
+ const message = formatStartupFailure(reason);
1089
1195
  notice.textContent = 'ForgeaX game failed to start.\\n' + message;
1090
1196
  notice.style.display = 'grid';
1091
1197
  };
@@ -1100,29 +1206,32 @@ function htmlSource(title) {
1100
1206
  }
1101
1207
  async function createViteConfig(facts, command2, base = "/", options = {}) {
1102
1208
  const bootstrapRoot = options.bootstrapRoot ?? "project-bootstrap";
1103
- const canonicalKit = bootstrapRoot === "resource-bootstrap" ? await resolveCanonicalKit() : void 0;
1209
+ const canonicalKit = await resolveCanonicalKit();
1104
1210
  const generated = resolve(facts.root, ".forgeax", "generated");
1105
1211
  await mkdir(generated, { recursive: true });
1212
+ const projectRoots = facts.assetRoots.map((root) => resolve(facts.root, root));
1213
+ const ignorePath = bootstrapRoot === "resource-bootstrap" ? isResourcePreviewIgnoredPath : isProjectSourceIgnoredPath;
1214
+ const builtinPack = await prepareBuiltinPack(projectRoots, generated, ignorePath);
1106
1215
  await Promise.all([
1107
1216
  writeFile(resolve(generated, "index.html"), htmlSource(facts.name)),
1108
1217
  writeFile(resolve(generated, "main.ts"), hostSource(facts, bootstrapRoot, canonicalKit?.guid))
1109
1218
  ]);
1110
1219
  const roots = [
1111
- ...facts.assetRoots.map((root) => resolve(facts.root, root)),
1220
+ ...projectRoots,
1221
+ ...builtinPack === void 0 ? [] : [builtinPack],
1112
1222
  ...canonicalKit === void 0 ? [] : [canonicalKit.root]
1113
1223
  ];
1114
1224
  const importers = [...projectImporters(facts)];
1115
1225
  const runtimeBinding = createStandaloneRuntimeAssetBinding(facts.id);
1116
1226
  const engineWorkspaceResolver = await createEngineWorkspaceResolver();
1227
+ const consumerAliases = await consumerEngineAliases(facts.root);
1117
1228
  const plugins = [
1118
1229
  ...engineWorkspaceResolver === void 0 ? [] : [engineWorkspaceResolver],
1119
1230
  forgeaxShader(),
1120
1231
  pluginPack({
1121
1232
  roots,
1122
- base,
1123
1233
  runtimeBinding,
1124
1234
  ddc: devKitDdcRoots(facts.root),
1125
- scriptablePack: {},
1126
1235
  refresh: command2 === "serve" ? reloadAssetHost() : void 0,
1127
1236
  importers: [
1128
1237
  audioImporter,
@@ -1132,8 +1241,8 @@ async function createViteConfig(facts, command2, base = "/", options = {}) {
1132
1241
  fontImporter,
1133
1242
  ...importers
1134
1243
  ],
1135
- cookers: [createParticleCodeNativeCookerFromRoots(roots)],
1136
- ...bootstrapRoot === "resource-bootstrap" ? { ignorePath: isResourcePreviewIgnoredPath } : { ignorePath: isProjectSourceIgnoredPath }
1244
+ cookers: [createMaterialPackCooker(roots), createParticleCodeNativeCookerFromRoots(roots)],
1245
+ ignorePath
1137
1246
  })
1138
1247
  ];
1139
1248
  return {
@@ -1142,8 +1251,11 @@ async function createViteConfig(facts, command2, base = "/", options = {}) {
1142
1251
  configFile: false,
1143
1252
  publicDir: facts.assetPublicDir === void 0 ? false : resolve(facts.root, facts.assetPublicDir),
1144
1253
  plugins,
1145
- resolve: { dedupe: ["@forgeax/engine-app", "@forgeax/engine-ecs", "@forgeax/engine-runtime"] },
1146
- server: { fs: { allow: [facts.root, ...roots] } },
1254
+ resolve: {
1255
+ alias: consumerAliases,
1256
+ dedupe: ["@forgeax/engine"]
1257
+ },
1258
+ server: { ...options.server, fs: { allow: [facts.root, ...roots] } },
1147
1259
  build: {
1148
1260
  target: "esnext",
1149
1261
  outDir: options.outDir ?? resolve(facts.root, "dist"),
@@ -1167,7 +1279,7 @@ async function readJson(path) {
1167
1279
  function firstUnsupportedStandaloneRealm(entries2, inheritedRealm = "engine") {
1168
1280
  for (const entry of entries2) {
1169
1281
  const realm = entry.realm ?? inheritedRealm ?? "engine";
1170
- if (realm === "host") return { id: entry.id, realm };
1282
+ if (realm !== "engine") return { id: entry.id, realm };
1171
1283
  if (entry.group === true) {
1172
1284
  const unsupported = firstUnsupportedStandaloneRealm(
1173
1285
  entry.config,
@@ -1285,13 +1397,10 @@ async function readProjectFacts(rootInput = process.cwd()) {
1285
1397
  }
1286
1398
  };
1287
1399
  }
1288
- function isCommandError(value) {
1289
- if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
1290
- const candidate = value;
1291
- return typeof candidate.code === "string" && typeof candidate.expected === "string" && typeof candidate.hint === "string" && candidate.detail !== null && typeof candidate.detail === "object" && !Array.isArray(candidate.detail);
1292
- }
1293
1400
  function commandError(cause, fallbackCode) {
1294
- 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
+ }
1295
1404
  return {
1296
1405
  code: fallbackCode,
1297
1406
  expected: "the ForgeaX command to complete",
@@ -1303,16 +1412,33 @@ var init_project = __esm({
1303
1412
  "src/project.ts"() {
1304
1413
  }
1305
1414
  });
1306
- function failure(error) {
1307
- 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
+ };
1308
1432
  }
1309
- function producerFailure(stderr, fallback, detail) {
1310
- try {
1311
- const parsed = JSON.parse(stderr.at(-1) ?? "");
1312
- if (isCommandError(parsed)) return failure(parsed);
1313
- } catch {
1433
+ function resolveProjectPort(port) {
1434
+ return { port: port ?? 5173, strictPort: port !== 0 };
1435
+ }
1436
+ var init_types = __esm({
1437
+ "src/types.ts"() {
1314
1438
  }
1315
- return failure({ ...fallback, detail: { ...detail, diagnostic: stderr.join("\n") } });
1439
+ });
1440
+ function failure(error) {
1441
+ return { ok: false, error };
1316
1442
  }
1317
1443
  async function sourcesAt(path) {
1318
1444
  const info = await stat(path);
@@ -1397,15 +1523,16 @@ async function addGltf(sourcePath, dryRun2) {
1397
1523
  stderrWrite: (line) => stderr.push(line)
1398
1524
  });
1399
1525
  if (exitCode !== 0) {
1400
- return producerFailure(
1401
- stderr,
1402
- {
1526
+ try {
1527
+ return failure(JSON.parse(stderr.at(-1) ?? ""));
1528
+ } catch {
1529
+ return failure({
1403
1530
  code: "asset-add-failed",
1404
1531
  expected: "the glTF producer to create or reuse a valid sidecar",
1405
- hint: "Inspect the glTF source and its external references."
1406
- },
1407
- { source: sourcePath }
1408
- );
1532
+ hint: "Inspect the glTF source and its external references.",
1533
+ detail: { source: sourcePath, diagnostic: stderr.join("\n") }
1534
+ });
1535
+ }
1409
1536
  }
1410
1537
  return { ok: true, value: { source: sourcePath, metaPath: `${sourcePath}.meta.json` } };
1411
1538
  }
@@ -1448,15 +1575,16 @@ async function entries(options) {
1448
1575
  { stdoutWrite: (line) => stdout.push(line), stderrWrite: (line) => stderr.push(line) }
1449
1576
  );
1450
1577
  if (!result.ok) {
1451
- return producerFailure(
1452
- stderr,
1453
- {
1578
+ try {
1579
+ return failure(JSON.parse(stderr.at(-1) ?? ""));
1580
+ } catch {
1581
+ return failure({
1454
1582
  code: "asset-authority-invalid",
1455
1583
  expected: "all asset roots and sidecars to pass the pack scanner",
1456
- hint: "Repair the first invalid asset authority reported by the scanner."
1457
- },
1458
- {}
1459
- );
1584
+ hint: "Repair the first invalid asset authority reported by the scanner.",
1585
+ detail: { diagnostic: stderr.join("\n") }
1586
+ });
1587
+ }
1460
1588
  }
1461
1589
  return { ok: true, value: { facts: facts.value, entries: result.value } };
1462
1590
  }
@@ -1541,7 +1669,6 @@ var init_package = __esm({
1541
1669
  "@forgeax/engine-audio-webaudio": "workspace:*",
1542
1670
  "@forgeax/engine-fbx": "workspace:*",
1543
1671
  "@forgeax/engine-font": "workspace:*",
1544
- "@forgeax/engine-geometry": "workspace:*",
1545
1672
  "@forgeax/engine-gltf": "workspace:*",
1546
1673
  "@forgeax/engine-image": "workspace:*",
1547
1674
  "@forgeax/engine-input": "workspace:*",
@@ -1551,6 +1678,7 @@ var init_package = __esm({
1551
1678
  "@forgeax/engine-preview": "workspace:*",
1552
1679
  "@forgeax/engine-project": "workspace:*",
1553
1680
  "@forgeax/engine-rhi-debug": "workspace:*",
1681
+ "@forgeax/engine-rhi-null": "workspace:*",
1554
1682
  "@forgeax/engine-render": "workspace:*",
1555
1683
  "@forgeax/engine-render-graph": "workspace:*",
1556
1684
  "@forgeax/engine-runtime": "workspace:*",
@@ -1568,7 +1696,6 @@ var init_package = __esm({
1568
1696
  vitest: "4.1.5"
1569
1697
  },
1570
1698
  devDependencies: {
1571
- "@forgeax/engine-rhi-null": "workspace:*",
1572
1699
  "@types/node": "^20.14.0"
1573
1700
  },
1574
1701
  forgeax: {
@@ -1694,6 +1821,7 @@ function createInitPlan(facts, sdk) {
1694
1821
  value: {
1695
1822
  root: facts.root,
1696
1823
  version: sdk?.sdkVersion ?? devkitVersion,
1824
+ ...sdk === void 0 ? {} : { pnpmVersion: sdk.requirements.pnpm },
1697
1825
  archiveBacked: sdk !== void 0,
1698
1826
  dependencyChanges,
1699
1827
  scriptChanges
@@ -1714,23 +1842,8 @@ async function applyInitPlan(facts, plan, options) {
1714
1842
  for (const change of plan.scriptChanges) scripts[change.name] = change.to;
1715
1843
  manifest.scripts = scripts;
1716
1844
  if (plan.archiveBacked) {
1717
- manifest.packageManager = "pnpm@10.33.2";
1718
- const pnpmValue = manifest.pnpm;
1719
- const pnpm = pnpmValue !== null && typeof pnpmValue === "object" ? pnpmValue : {};
1720
- manifest.pnpm = {
1721
- ...pnpm,
1722
- onlyBuiltDependencies: [
1723
- "@forgeax/engine-codec",
1724
- "@forgeax/engine-fbx",
1725
- "@forgeax/engine-wgpu-wasm",
1726
- "esbuild"
1727
- ],
1728
- supportedArchitectures: {
1729
- os: ["darwin", "linux", "win32"],
1730
- cpu: ["x64", "arm64"],
1731
- libc: ["glibc", "musl"]
1732
- }
1733
- };
1845
+ if (plan.pnpmVersion === void 0) throw new Error("sdk-pnpm-version-missing");
1846
+ manifest.packageManager = `pnpm@${plan.pnpmVersion}`;
1734
1847
  }
1735
1848
  await writeFile(resolve(facts.root, "package.json"), `${JSON.stringify(manifest, null, 2)}
1736
1849
  `);
@@ -1796,6 +1909,270 @@ var init_sdk = __esm({
1796
1909
  "src/sdk.ts"() {
1797
1910
  }
1798
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
+ }
1926
+ function sdkInitPath(sdk) {
1927
+ return resolve(sdk.root, ...SDK_INIT_PATH);
1928
+ }
1929
+ function commandFailure(code, expected, hint, detail = {}) {
1930
+ return { ok: false, error: { code, expected, hint, detail } };
1931
+ }
1932
+ function sdkProjectInstallArgs(store) {
1933
+ const common = [
1934
+ "install",
1935
+ "--frozen-lockfile",
1936
+ "--ignore-scripts",
1937
+ "--side-effects-cache=true",
1938
+ "--child-concurrency=1"
1939
+ ];
1940
+ return store === void 0 ? common : [...common, "--offline", "--config.trust-lockfile=true", "--store-dir", store];
1941
+ }
1942
+ function sdkBootstrapInstallArgs(store) {
1943
+ const common = [
1944
+ "install",
1945
+ "--frozen-lockfile",
1946
+ "--child-concurrency=1",
1947
+ "--side-effects-cache=true"
1948
+ ];
1949
+ return store === void 0 ? common : [...common, "--offline", "--config.trust-lockfile=true", "--store-dir", store];
1950
+ }
1951
+ function supportedPnpm(version) {
1952
+ const [major = 0, minor = 0] = version.split(".").map(Number);
1953
+ return major === 11 && minor >= 7;
1954
+ }
1955
+ async function currentPnpm() {
1956
+ try {
1957
+ const result = await execFileAsync("pnpm", ["--version"], { maxBuffer: 1024 * 1024 });
1958
+ const version = result.stdout.trim();
1959
+ if (version.length === 0) throw new Error("pnpm returned an empty version");
1960
+ return { ok: true, value: version };
1961
+ } catch (cause) {
1962
+ return commandFailure(
1963
+ "pnpm-unavailable",
1964
+ "pnpm 11.7.0 or newer in the pnpm 11 line to be available on PATH",
1965
+ "Install or activate pnpm 11, then rerun forgeax init from the SDK root.",
1966
+ { reason: cause instanceof Error ? cause.message : String(cause) }
1967
+ );
1968
+ }
1969
+ }
1970
+ function matchesState(sdk, state) {
1971
+ if (state === null || typeof state !== "object" || Array.isArray(state)) return false;
1972
+ const value = state;
1973
+ return value.schemaVersion === SDK_INIT_SCHEMA_VERSION && value.sdkVersion === sdk.manifest.sdkVersion && value.engineCommit === sdk.manifest.engineCommit && typeof value.pnpm === "string" && supportedPnpm(value.pnpm) && value.node === process.versions.node && value.platform === process.platform && value.arch === process.arch;
1974
+ }
1975
+ async function readSdkInitState(sdk) {
1976
+ try {
1977
+ const value = JSON.parse(await readFile(sdkInitPath(sdk), "utf8"));
1978
+ return matchesState(sdk, value) ? value : void 0;
1979
+ } catch {
1980
+ return void 0;
1981
+ }
1982
+ }
1983
+ async function requireSdkInitialization(sdk) {
1984
+ const state = await readSdkInitState(sdk);
1985
+ if (state === void 0) {
1986
+ return commandFailure(
1987
+ "sdk-not-initialized",
1988
+ "the downloaded SDK to be initialized for this Node/pnpm/platform tuple",
1989
+ "Run ./bin/forgeax init from the SDK root once, then run forgeax new outside the SDK.",
1990
+ {
1991
+ sdkRoot: sdk.root,
1992
+ sdkVersion: sdk.manifest.sdkVersion,
1993
+ pnpm: sdk.manifest.requirements.pnpm,
1994
+ node: process.versions.node,
1995
+ platform: process.platform,
1996
+ arch: process.arch
1997
+ }
1998
+ );
1999
+ }
2000
+ return { ok: true, value: state };
2001
+ }
2002
+ async function copyBootstrapInputs(sdk, root) {
2003
+ const template = sdk.templates.get(sdk.defaultTemplate);
2004
+ if (template === void 0) throw new Error("sdk-default-template-missing");
2005
+ for (const name of ["package.json", "pnpm-lock.yaml", "pnpm-workspace.yaml"]) {
2006
+ await cp(resolve(template, name), resolve(root, name));
2007
+ }
2008
+ try {
2009
+ await cp(resolve(template, ".npmrc"), resolve(root, ".npmrc"));
2010
+ } catch (cause) {
2011
+ if (cause === null || typeof cause !== "object" || !("code" in cause) || cause.code !== "ENOENT") {
2012
+ throw cause;
2013
+ }
2014
+ }
2015
+ }
2016
+ async function sdkInitCommand(sdk, options = {}) {
2017
+ const pnpmResult = await currentPnpm();
2018
+ if (!pnpmResult.ok) return pnpmResult;
2019
+ if (!supportedPnpm(pnpmResult.value)) {
2020
+ return commandFailure(
2021
+ "pnpm-version-unsupported",
2022
+ "pnpm >=11.7.0 <12",
2023
+ "Activate pnpm 11.7.0 or newer in the pnpm 11 line, then rerun forgeax init.",
2024
+ { actual: pnpmResult.value, expected: sdk.manifest.requirements.pnpm }
2025
+ );
2026
+ }
2027
+ const state = {
2028
+ schemaVersion: SDK_INIT_SCHEMA_VERSION,
2029
+ sdkVersion: sdk.manifest.sdkVersion,
2030
+ engineCommit: sdk.manifest.engineCommit,
2031
+ pnpm: pnpmResult.value,
2032
+ node: process.versions.node,
2033
+ platform: process.platform,
2034
+ arch: process.arch
2035
+ };
2036
+ const report = {
2037
+ ...state,
2038
+ root: sdk.root,
2039
+ store: sdk.store === void 0 ? "registry" : "offline",
2040
+ onboarding: agentOnboarding(sdk)
2041
+ };
2042
+ if (options.dryRun === true || options.install === false) return { ok: true, value: report };
2043
+ let staging;
2044
+ try {
2045
+ staging = await mkdtemp(resolve(tmpdir(), "forgeax-sdk-init-"));
2046
+ await copyBootstrapInputs(sdk, staging);
2047
+ await execFileAsync("pnpm", sdkBootstrapInstallArgs(sdk.store), {
2048
+ cwd: staging,
2049
+ env: { ...process.env, CI: "true" },
2050
+ maxBuffer: 16 * 1024 * 1024
2051
+ });
2052
+ await mkdir(resolve(sdk.root, ".forgeax"), { recursive: true });
2053
+ await writeFile(sdkInitPath(sdk), `${JSON.stringify(state, null, 2)}
2054
+ `);
2055
+ return { ok: true, value: report };
2056
+ } catch (cause) {
2057
+ return commandFailure(
2058
+ "sdk-init-failed",
2059
+ "the SDK dependency closure to install and build native packages successfully",
2060
+ "Inspect the pnpm output, repair Node/pnpm or platform permissions, then rerun forgeax init from the SDK root.",
2061
+ { reason: cause instanceof Error ? cause.message : String(cause), sdkRoot: sdk.root }
2062
+ );
2063
+ } finally {
2064
+ if (staging !== void 0) await rm(staging, { recursive: true, force: true });
2065
+ }
2066
+ }
2067
+ var execFileAsync, SDK_INIT_PATH, SDK_INIT_SCHEMA_VERSION;
2068
+ var init_sdk_bootstrap = __esm({
2069
+ "src/sdk-bootstrap.ts"() {
2070
+ execFileAsync = promisify(execFile);
2071
+ SDK_INIT_PATH = [".forgeax", "sdk-init.json"];
2072
+ SDK_INIT_SCHEMA_VERSION = "1.0.0";
2073
+ }
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
+ });
1799
2176
  function slash(path) {
1800
2177
  return path.split(sep).join("/");
1801
2178
  }
@@ -2161,6 +2538,20 @@ async function canonicalProspectivePath(path) {
2161
2538
  }
2162
2539
  }
2163
2540
  }
2541
+ async function configureSdkStore(root, store) {
2542
+ if (store === void 0) return;
2543
+ const path = resolve(root, ".npmrc");
2544
+ let content = "";
2545
+ try {
2546
+ content = await readFile(path, "utf8");
2547
+ } catch (cause) {
2548
+ if (!isMissingPathError(cause)) throw cause;
2549
+ }
2550
+ const lines = content.split(/\r?\n/).filter((line) => line.trim().length > 0 && !/^\s*store-dir\s*=/.test(line));
2551
+ lines.push(`store-dir=${store}`);
2552
+ await writeFile(path, `${lines.join("\n")}
2553
+ `);
2554
+ }
2164
2555
  function containsOrEquals(parent, child) {
2165
2556
  const path = relative(parent, child);
2166
2557
  return path === "" || path !== ".." && !path.startsWith(`..${sep}`) && !isAbsolute(path);
@@ -2171,25 +2562,14 @@ function nodeSupported() {
2171
2562
  }
2172
2563
  function pnpmSupported(version) {
2173
2564
  const [major = 0, minor = 0] = version.split(".").map(Number);
2174
- return major === 10 && minor >= 33;
2175
- }
2176
- function sdkInstallArgs(store) {
2177
- if (store === void 0) return ["install", "--frozen-lockfile"];
2178
- return [
2179
- "install",
2180
- "--offline",
2181
- "--frozen-lockfile",
2182
- "--side-effects-cache=false",
2183
- "--store-dir",
2184
- store
2185
- ];
2565
+ return major === 11 && minor >= 7;
2186
2566
  }
2187
2567
  async function doctorCommand(options = {}) {
2188
2568
  const facts = await readProjectFacts(options.root);
2189
2569
  if (!facts.ok) return facts;
2190
2570
  let pnpm;
2191
2571
  try {
2192
- pnpm = (await execFileAsync("pnpm", ["--version"], { cwd: facts.value.root })).stdout.trim();
2572
+ pnpm = (await execFileAsync2("pnpm", ["--version"], { cwd: facts.value.root })).stdout.trim();
2193
2573
  } catch (cause) {
2194
2574
  return {
2195
2575
  ok: false,
@@ -2217,7 +2597,7 @@ async function doctorCommand(options = {}) {
2217
2597
  ok: false,
2218
2598
  error: {
2219
2599
  code: "pnpm-version-unsupported",
2220
- expected: "pnpm >=10.33.0 <11",
2600
+ expected: "pnpm >=11.7.0 <12",
2221
2601
  hint: "Enable the packageManager-declared pnpm version with Corepack and retry.",
2222
2602
  detail: { actual: pnpm }
2223
2603
  }
@@ -2256,12 +2636,20 @@ async function doctorCommand(options = {}) {
2256
2636
  };
2257
2637
  }
2258
2638
  async function initCommand(options = {}) {
2259
- const facts = await readProjectFacts(options.root);
2260
- if (!facts.ok) return facts;
2261
2639
  try {
2262
2640
  const sdk = await findSdkContext();
2641
+ const root = await canonicalProspectivePath(options.root ?? process.cwd());
2642
+ if (sdk !== void 0 && root === await canonicalProspectivePath(sdk.root)) {
2643
+ return sdkInitCommand(sdk, options);
2644
+ }
2645
+ const facts = await readProjectFacts(root);
2646
+ if (!facts.ok) return facts;
2263
2647
  const plan = createInitPlan(facts.value, sdk?.manifest);
2264
2648
  if (!plan.ok) return plan;
2649
+ if (sdk !== void 0 && options.dryRun !== true) {
2650
+ const initialized = await requireSdkInitialization(sdk);
2651
+ if (!initialized.ok) return initialized;
2652
+ }
2265
2653
  const applied = await applyInitPlan(facts.value, plan.value, options);
2266
2654
  if (!applied.ok || options.dryRun === true) return applied;
2267
2655
  if (sdk !== void 0) {
@@ -2271,12 +2659,31 @@ async function initCommand(options = {}) {
2271
2659
  resolve(template, "pnpm-lock.yaml"),
2272
2660
  resolve(facts.value.root, "pnpm-lock.yaml")
2273
2661
  );
2662
+ await copyFile(
2663
+ resolve(template, "pnpm-workspace.yaml"),
2664
+ resolve(facts.value.root, "pnpm-workspace.yaml")
2665
+ );
2666
+ try {
2667
+ await readFile(resolve(facts.value.root, ".npmrc"), "utf8");
2668
+ } catch (cause) {
2669
+ if (!isMissingPathError(cause)) throw cause;
2670
+ try {
2671
+ await copyFile(resolve(template, ".npmrc"), resolve(facts.value.root, ".npmrc"));
2672
+ } catch (templateCause) {
2673
+ if (!isMissingPathError(templateCause)) throw templateCause;
2674
+ }
2675
+ }
2274
2676
  await copySdkSkills(sdk, facts.value.root);
2275
2677
  await installProjectSkills(facts.value.root, sdk.manifest);
2678
+ await configureSdkStore(
2679
+ facts.value.root,
2680
+ sdk.store === void 0 ? void 0 : await canonicalProspectivePath(sdk.store)
2681
+ );
2276
2682
  }
2277
2683
  if (options.install === false) return applied;
2278
- const installArgs = sdk === void 0 ? ["install", "--frozen-lockfile=false"] : sdkInstallArgs(sdk.store);
2279
- await execFileAsync("pnpm", installArgs, {
2684
+ const store = sdk === void 0 || sdk.store === void 0 ? void 0 : await canonicalProspectivePath(sdk.store);
2685
+ const installArgs = sdk === void 0 ? ["install", "--frozen-lockfile=false"] : sdkProjectInstallArgs(store);
2686
+ await execFileAsync2("pnpm", installArgs, {
2280
2687
  cwd: facts.value.root,
2281
2688
  env: { ...process.env, CI: "true" },
2282
2689
  maxBuffer: 16 * 1024 * 1024
@@ -2354,12 +2761,15 @@ async function newCommand(options = {}) {
2354
2761
  value: { root, template: templateId, sdkVersion: sdk.manifest.sdkVersion }
2355
2762
  };
2356
2763
  }
2764
+ const initialized = await requireSdkInitialization(sdk);
2765
+ if (!initialized.ok) return initialized;
2357
2766
  const parent = dirname(root);
2358
2767
  await mkdir(parent, { recursive: true });
2359
2768
  let staging = await mkdtemp(
2360
2769
  resolve(parent, `.${basename(root)}.forgeax-staging-`)
2361
2770
  );
2362
2771
  const committedNames = [];
2772
+ let committed = false;
2363
2773
  try {
2364
2774
  for (const name of await readdir(template)) {
2365
2775
  await cp(resolve(template, name), resolve(staging, name), {
@@ -2370,14 +2780,12 @@ async function newCommand(options = {}) {
2370
2780
  }
2371
2781
  await copySdkSkills(sdk, staging);
2372
2782
  await installProjectSkills(staging, sdk.manifest);
2373
- await execFileAsync("pnpm", sdkInstallArgs(sdk.store), {
2374
- cwd: staging,
2375
- env: { ...process.env, CI: "true" },
2376
- maxBuffer: 16 * 1024 * 1024
2377
- });
2783
+ const store = sdk.store === void 0 ? void 0 : await canonicalProspectivePath(sdk.store);
2784
+ await configureSdkStore(staging, store);
2378
2785
  if (!targetExists) {
2379
2786
  await rename(staging, root);
2380
2787
  staging = void 0;
2788
+ committedNames.push(...await readdir(root));
2381
2789
  } else {
2382
2790
  for (const name of await readdir(staging)) {
2383
2791
  await rename(resolve(staging, name), resolve(root, name));
@@ -2386,29 +2794,57 @@ async function newCommand(options = {}) {
2386
2794
  await rm(staging, { recursive: true, force: true });
2387
2795
  staging = void 0;
2388
2796
  }
2797
+ committed = true;
2798
+ await execFileAsync2("pnpm", sdkProjectInstallArgs(store), {
2799
+ cwd: root,
2800
+ env: { ...process.env, CI: "true" },
2801
+ maxBuffer: 16 * 1024 * 1024
2802
+ });
2803
+ const sdkUpdate = await checkSdkUpdate(sdk.manifest.sdkVersion);
2389
2804
  return {
2390
2805
  ok: true,
2391
- 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
+ }
2392
2813
  };
2393
2814
  } catch (cause) {
2394
2815
  if (staging !== void 0) await rm(staging, { recursive: true, force: true });
2395
- await Promise.all(
2396
- committedNames.map((name) => rm(resolve(root, name), { recursive: true, force: true }))
2397
- );
2816
+ if (committed) {
2817
+ if (!targetExists) {
2818
+ await rm(root, { recursive: true, force: true });
2819
+ return { ok: false, error: commandError(cause, "project-create-failed") };
2820
+ }
2821
+ const cleanupNames = /* @__PURE__ */ new Set([...committedNames, "node_modules"]);
2822
+ await Promise.all(
2823
+ [...cleanupNames].map(
2824
+ (name) => rm(resolve(root, name), { recursive: true, force: true })
2825
+ )
2826
+ );
2827
+ } else {
2828
+ await Promise.all(
2829
+ committedNames.map((name) => rm(resolve(root, name), { recursive: true, force: true }))
2830
+ );
2831
+ }
2398
2832
  return { ok: false, error: commandError(cause, "project-create-failed") };
2399
2833
  }
2400
2834
  } catch (cause) {
2401
2835
  return { ok: false, error: commandError(cause, "project-create-failed") };
2402
2836
  }
2403
2837
  }
2404
- var execFileAsync;
2838
+ var execFileAsync2;
2405
2839
  var init_bootstrap_commands = __esm({
2406
2840
  "src/bootstrap-commands.ts"() {
2407
2841
  init_init();
2408
2842
  init_project();
2409
2843
  init_sdk();
2844
+ init_sdk_bootstrap();
2845
+ init_sdk_update();
2410
2846
  init_skill_install();
2411
- execFileAsync = promisify(execFile);
2847
+ execFileAsync2 = promisify(execFile);
2412
2848
  }
2413
2849
  });
2414
2850
 
@@ -2467,7 +2903,7 @@ function withoutEntry(entries2, id) {
2467
2903
  }
2468
2904
  async function mutateDependency(root, action, dependency) {
2469
2905
  if (dependency === void 0) return;
2470
- await execFileAsync2("pnpm", [action, dependency], { cwd: root, maxBuffer: 16 * 1024 * 1024 });
2906
+ await execFileAsync3("pnpm", [action, dependency], { cwd: root, maxBuffer: 16 * 1024 * 1024 });
2471
2907
  }
2472
2908
  async function pluginInstallCommand(options) {
2473
2909
  const root = resolve(options.root ?? process.cwd());
@@ -2569,489 +3005,10 @@ async function pluginUninstallCommand(options) {
2569
3005
  };
2570
3006
  }
2571
3007
  }
2572
- var execFileAsync2;
3008
+ var execFileAsync3;
2573
3009
  var init_plugin_authoring = __esm({
2574
3010
  "src/plugin-authoring.ts"() {
2575
- execFileAsync2 = promisify(execFile);
2576
- }
2577
- });
2578
- var RhiError;
2579
- var init_dist2 = __esm({
2580
- "../rhi/dist/index.mjs"() {
2581
- RhiError = class extends Error {
2582
- code;
2583
- expected;
2584
- hint;
2585
- detail;
2586
- constructor(args2) {
2587
- super(`[RhiError ${args2.code}] expected: ${args2.expected}; hint: ${args2.hint}`);
2588
- this.name = "RhiError";
2589
- this.code = args2.code;
2590
- this.expected = args2.expected;
2591
- this.hint = args2.hint;
2592
- this.detail = args2.detail;
2593
- }
2594
- };
2595
- }
2596
- });
2597
- function readPassLabel(desc) {
2598
- if (desc && typeof desc.label === "string" && desc.label.length > 0) {
2599
- return desc.label;
2600
- }
2601
- return "<unnamed>";
2602
- }
2603
- function readRecord(handle) {
2604
- if (handle === null || typeof handle !== "object") return void 0;
2605
- const tagged = handle;
2606
- return tagged[BOOKKEEPING_KEY];
2607
- }
2608
- function isHandleDestroyed(handle) {
2609
- if (readRecord(handle)?.destroyed === true) {
2610
- return err(
2611
- new RhiError({
2612
- code: "destroy-after-destroy",
2613
- expected: "GPU buffer/texture handle has not been destroyed yet",
2614
- hint: "object already destroyed; track lifecycle in caller or check isDestroyed before re-destroy"
2615
- })
2616
- );
2617
- }
2618
- return ok(void 0);
2619
- }
2620
- function acquireCanvasContext(_canvas) {
2621
- return ok(new RhiNullCanvasContext());
2622
- }
2623
- function createShaderModule(_device, _desc) {
2624
- return Promise.resolve(ok({}));
2625
- }
2626
- function requestAdapter(_opts, _compatibleSurface) {
2627
- return Promise.resolve(ok(new RhiNullAdapter()));
2628
- }
2629
- var RhiNullRenderPassEncoder, RhiNullComputePassEncoder, DeviceCounter, RhiNullCommandEncoder, BOOKKEEPING_KEY, Bookkeeper, nextDeviceId, RhiNullDevice, EMPTY_FEATURES, EMPTY_LIMITS, NEVER, RhiNullQueue, RhiNullAdapter, RhiNullCanvasContext, rhi;
2630
- var init_dist3 = __esm({
2631
- "../rhi-null/dist/index.mjs"() {
2632
- init_dist2();
2633
- RhiNullRenderPassEncoder = class {
2634
- /** Number of draw* calls issued on this pass (AC-06 readback). */
2635
- drawCount = 0;
2636
- bindGroupCount = 0;
2637
- /** Most recent setVertexBuffer / setBindGroup ownership validation; ok unless
2638
- * a foreign handle was passed (AC-09 readback). */
2639
- lastValidation = ok(void 0);
2640
- bookkeeper;
2641
- counter;
2642
- passName;
2643
- constructor(bookkeeper, counter, passName) {
2644
- this.bookkeeper = bookkeeper;
2645
- this.counter = counter;
2646
- this.passName = passName;
2647
- }
2648
- setPipeline(_pipeline) {
2649
- }
2650
- setVertexBuffer(_slot, buffer, _offset, _size) {
2651
- this.lastValidation = this.bookkeeper.validateOwnership(buffer);
2652
- }
2653
- setIndexBuffer(_buffer, _format, _offset, _size) {
2654
- }
2655
- setBindGroup(_index, bindGroup, _dynamicOffsetsData, _dynamicOffsetsDataStart, _dynamicOffsetsDataLength) {
2656
- this.bindGroupCount++;
2657
- this.counter?.recordBindGroup();
2658
- this.lastValidation = this.bookkeeper.validateOwnership(bindGroup);
2659
- }
2660
- draw(_vertexCount, _instanceCount, _firstVertex, _firstInstance) {
2661
- this.drawCount++;
2662
- this.counter?.recordDraw();
2663
- }
2664
- drawIndexed(_indexCount, _instanceCount, _firstIndex, _baseVertex, _firstInstance) {
2665
- this.drawCount++;
2666
- this.counter?.recordDraw();
2667
- }
2668
- end() {
2669
- this.counter?.recordPassName(this.passName);
2670
- }
2671
- setViewport(_x, _y, _w, _h, _minDepth, _maxDepth) {
2672
- }
2673
- setScissorRect(_x, _y, _w, _h) {
2674
- }
2675
- setBlendConstant(_color) {
2676
- }
2677
- setStencilReference(_reference) {
2678
- }
2679
- drawIndirect(_indirectBuffer, _indirectOffset) {
2680
- this.drawCount++;
2681
- this.counter?.recordDraw();
2682
- }
2683
- drawIndexedIndirect(_indirectBuffer, _indirectOffset) {
2684
- this.drawCount++;
2685
- this.counter?.recordDraw();
2686
- }
2687
- pushDebugGroup(_groupLabel) {
2688
- }
2689
- popDebugGroup() {
2690
- }
2691
- insertDebugMarker(_markerLabel) {
2692
- }
2693
- executeBundles(_bundles) {
2694
- return ok(void 0);
2695
- }
2696
- beginOcclusionQuery(_queryIndex) {
2697
- return ok(void 0);
2698
- }
2699
- endOcclusionQuery() {
2700
- return ok(void 0);
2701
- }
2702
- };
2703
- RhiNullComputePassEncoder = class {
2704
- /** Number of dispatchWorkgroups calls issued on this pass (readback). */
2705
- dispatchCount = 0;
2706
- /** Most recent setBindGroup ownership validation (AC-09 readback). */
2707
- lastValidation = ok(void 0);
2708
- bookkeeper;
2709
- counter;
2710
- passName;
2711
- constructor(bookkeeper, counter, passName) {
2712
- this.bookkeeper = bookkeeper;
2713
- this.counter = counter;
2714
- this.passName = passName;
2715
- }
2716
- setPipeline(_pipeline) {
2717
- }
2718
- setBindGroup(_index, bindGroup, _dynamicOffsets) {
2719
- this.lastValidation = this.bookkeeper.validateOwnership(bindGroup);
2720
- }
2721
- dispatchWorkgroups(_x, _y, _z) {
2722
- this.dispatchCount++;
2723
- this.counter?.recordDispatch();
2724
- }
2725
- dispatchWorkgroupsIndirect(indirectBuffer, _indirectOffset) {
2726
- this.lastValidation = this.bookkeeper.validateOwnership(indirectBuffer);
2727
- this.dispatchCount++;
2728
- this.counter?.recordDispatch();
2729
- }
2730
- end() {
2731
- this.counter?.recordPassName(this.passName);
2732
- }
2733
- };
2734
- DeviceCounter = class {
2735
- constructor(device) {
2736
- this.device = device;
2737
- }
2738
- device;
2739
- recordDraw() {
2740
- this.device.totalDrawCount++;
2741
- }
2742
- recordDispatch() {
2743
- this.device.totalDispatchCount++;
2744
- }
2745
- recordBindGroup() {
2746
- this.device.totalBindGroupCount++;
2747
- }
2748
- recordPassName(name) {
2749
- this.device.framePassNames.push(name);
2750
- }
2751
- };
2752
- RhiNullCommandEncoder = class {
2753
- bookkeeper;
2754
- counter;
2755
- constructor(bookkeeper, device) {
2756
- this.bookkeeper = bookkeeper;
2757
- this.counter = new DeviceCounter(device);
2758
- }
2759
- beginRenderPass(desc) {
2760
- const label = readPassLabel(desc);
2761
- return new RhiNullRenderPassEncoder(this.bookkeeper, this.counter, label);
2762
- }
2763
- beginComputePass(desc) {
2764
- const label = readPassLabel(desc);
2765
- return new RhiNullComputePassEncoder(this.bookkeeper, this.counter, label);
2766
- }
2767
- copyBufferToBuffer(_source, _sourceOffsetOrDestination, _destinationOrSize, _destinationOffset, _size) {
2768
- }
2769
- copyBufferToTexture(_source, _destination, _copySize) {
2770
- }
2771
- copyTextureToBuffer(_source, _destination, _copySize) {
2772
- }
2773
- copyTextureToTexture(_source, _destination, _copySize) {
2774
- }
2775
- clearBuffer(_buffer, _offset, _size) {
2776
- }
2777
- resolveQuerySet(_querySet, _firstQuery, _queryCount, _destination, _destinationOffset) {
2778
- return ok(void 0);
2779
- }
2780
- writeTimestamp(_querySet, _queryIndex) {
2781
- }
2782
- pushDebugGroup(_groupLabel) {
2783
- }
2784
- popDebugGroup() {
2785
- }
2786
- insertDebugMarker(_markerLabel) {
2787
- }
2788
- finish() {
2789
- return ok(this.bookkeeper.register("CommandBuffer"));
2790
- }
2791
- };
2792
- BOOKKEEPING_KEY = /* @__PURE__ */ Symbol("forgeax-rhi-null-bookkeeping");
2793
- Bookkeeper = class {
2794
- deviceId;
2795
- nextHandleId = 0;
2796
- records = /* @__PURE__ */ new Map();
2797
- constructor(deviceId) {
2798
- this.deviceId = deviceId;
2799
- }
2800
- /**
2801
- * Register a freshly-minted handle of the given kind, returning a plain
2802
- * object that carries its ledger row. The caller casts the return value to
2803
- * the concrete brand (`as unknown as Buffer` etc.).
2804
- */
2805
- register(kind) {
2806
- const id = this.nextHandleId++;
2807
- const record = {
2808
- id,
2809
- kind,
2810
- destroyed: false,
2811
- sourceDeviceId: this.deviceId
2812
- };
2813
- this.records.set(id, record);
2814
- return { [BOOKKEEPING_KEY]: record };
2815
- }
2816
- /**
2817
- * Mark a handle destroyed. Fail-fasts on a second destroy
2818
- * ('destroy-after-destroy') or on a handle issued by a different device
2819
- * ('rhi-not-available'); otherwise flips the destroyed flag and returns ok.
2820
- */
2821
- destroy(handle) {
2822
- const ownership = this.validateOwnership(handle);
2823
- if (!ownership.ok) return ownership;
2824
- const destroyedCheck = isHandleDestroyed(handle);
2825
- if (!destroyedCheck.ok) return destroyedCheck;
2826
- ownership.value.destroyed = true;
2827
- return ok(void 0);
2828
- }
2829
- /**
2830
- * Report whether a handle has already been destroyed (true once destroy()
2831
- * has flipped its flag). Used by command-stream methods that must not
2832
- * consume a stale handle.
2833
- */
2834
- isDestroyed(handle) {
2835
- return readRecord(handle)?.destroyed === true;
2836
- }
2837
- /**
2838
- * Validate that a handle was issued by THIS device. Returns the ledger row on
2839
- * success so callers can mutate it (e.g. flip destroyed); returns
2840
- * 'rhi-not-available' for a foreign handle (AC-09 — no silent pass).
2841
- */
2842
- validateOwnership(handle) {
2843
- const record = readRecord(handle);
2844
- if (record === void 0 || record.sourceDeviceId !== this.deviceId) {
2845
- return err(
2846
- new RhiError({
2847
- code: "rhi-not-available",
2848
- expected: "handle was issued by this RhiNull device",
2849
- hint: "do not pass a handle created on a different RhiNull device into this device; create resources on the device they are used with"
2850
- })
2851
- );
2852
- }
2853
- return ok(record);
2854
- }
2855
- /**
2856
- * Return all ledger rows as a readonly array. M3 unit tests (w17) read this
2857
- * to assert create/destroy pairing, BGL/PSO shape counts, and resource
2858
- * lifecycle coverage (AC-05/06/07). Returns a snapshot of the current Map
2859
- * so callers can iterate without a stale reference after further mutations.
2860
- */
2861
- allRecords() {
2862
- return [...this.records.values()];
2863
- }
2864
- /** Report the total number of records in the ledger. */
2865
- recordCount() {
2866
- return this.records.size;
2867
- }
2868
- };
2869
- nextDeviceId = 0;
2870
- RhiNullDevice = class {
2871
- internalBookkeeper;
2872
- nullQueue;
2873
- encoderFactory;
2874
- /** Per-frame total draw count across all pass encoders executed this frame
2875
- * (aggregated by the command encoder on finish, then reset). M3 unit tests
2876
- * (w17) read this to assert draw count >= 1 (AC-06). */
2877
- totalDrawCount = 0;
2878
- /** Per-frame total direct and indirect compute dispatch count. */
2879
- totalDispatchCount = 0;
2880
- /** Per-frame total bind group set count (AC-06 / AC-05 readback). */
2881
- totalBindGroupCount = 0;
2882
- /** Per-frame pass names executed this frame, in schedule order (AC-04). */
2883
- framePassNames = [];
2884
- /** The per-device handle ledger — exposed so M3 tests can assert create/destroy
2885
- * pairing and BGL/PSO shape counts (AC-05/06/07). */
2886
- get bookkeeper() {
2887
- return this.internalBookkeeper;
2888
- }
2889
- constructor(queue, encoderFactory) {
2890
- this.internalBookkeeper = new Bookkeeper(nextDeviceId++);
2891
- this.nullQueue = queue;
2892
- this.encoderFactory = encoderFactory;
2893
- }
2894
- get caps() {
2895
- return {
2896
- backendKind: "null",
2897
- compute: true,
2898
- timestampQuery: false,
2899
- timestampPeriodNanoseconds: null,
2900
- indirectDrawing: true,
2901
- textureCompressionBc: false,
2902
- textureCompressionEtc2: false,
2903
- textureCompressionAstc: false,
2904
- // 3 wgpu-native-only reserved flags stay false on non-native backends
2905
- // (D-5); the headless backend is not a native runtime.
2906
- multiDrawIndirect: false,
2907
- pushConstants: false,
2908
- textureBindingArray: false,
2909
- samplerAliasing: true,
2910
- firstInstanceIndirect: true,
2911
- storageBuffer: true,
2912
- storageTexture: true,
2913
- rgba16floatRenderable: true,
2914
- rg11b10ufloatRenderable: true,
2915
- float32Filterable: true,
2916
- maxColorAttachments: 8
2917
- };
2918
- }
2919
- get features() {
2920
- return EMPTY_FEATURES;
2921
- }
2922
- get limits() {
2923
- return EMPTY_LIMITS;
2924
- }
2925
- get queue() {
2926
- return this.nullQueue;
2927
- }
2928
- // forgeax-async-whitelist: dom-native — spec `GPUDevice.lost` Promise
2929
- // passthrough. The headless backend never loses a device (no GPU), so the
2930
- // Promise stays unsettled for the lifetime of the device, mirroring a live
2931
- // device that never transitions to the lost state.
2932
- get lost() {
2933
- return NEVER;
2934
- }
2935
- createBuffer(_desc) {
2936
- return ok(this.internalBookkeeper.register("Buffer"));
2937
- }
2938
- createTexture(_desc) {
2939
- return ok(this.internalBookkeeper.register("Texture"));
2940
- }
2941
- destroyBuffer(buf) {
2942
- return this.internalBookkeeper.destroy(buf);
2943
- }
2944
- destroyQuerySet(querySet) {
2945
- return this.internalBookkeeper.destroy(querySet);
2946
- }
2947
- destroyTexture(tex) {
2948
- return this.internalBookkeeper.destroy(tex);
2949
- }
2950
- createTextureView(_texture, _desc) {
2951
- return ok(this.internalBookkeeper.register("TextureView"));
2952
- }
2953
- createSampler(_desc) {
2954
- return ok(this.internalBookkeeper.register("Sampler"));
2955
- }
2956
- createBindGroupLayout(_desc) {
2957
- return ok(this.internalBookkeeper.register("BindGroupLayout"));
2958
- }
2959
- createBindGroup(_desc) {
2960
- return ok(this.internalBookkeeper.register("BindGroup"));
2961
- }
2962
- createPipelineLayout(_desc) {
2963
- return ok(this.internalBookkeeper.register("PipelineLayout"));
2964
- }
2965
- createRenderPipeline(_desc) {
2966
- return ok(this.makePipeline("RenderPipeline"));
2967
- }
2968
- createComputePipeline(_desc) {
2969
- return ok(this.makePipeline("ComputePipeline"));
2970
- }
2971
- createQuerySet(desc) {
2972
- if (desc.type === "timestamp") {
2973
- return err(
2974
- new RhiError({
2975
- code: "feature-not-enabled",
2976
- expected: "caps.timestampQuery === true (timestamp-query feature)",
2977
- hint: "RhiNull is structural-only and cannot produce GPU timestamp ticks"
2978
- })
2979
- );
2980
- }
2981
- return ok(this.internalBookkeeper.register("QuerySet"));
2982
- }
2983
- createCommandEncoder(_desc) {
2984
- return ok(this.encoderFactory(this.internalBookkeeper, this));
2985
- }
2986
- /**
2987
- * Mint a pipeline handle whose object also carries the no-op
2988
- * `getBindGroupLayout(index)` ops method (D-2). The prod auto-layout path
2989
- * (debug-draw.ts) and the existing mock unit tests both call
2990
- * `pipeline.getBindGroupLayout(n)`; returning a legal BindGroupLayout brand
2991
- * (recorded in the ledger) keeps those consumers from crashing on a missing
2992
- * method.
2993
- */
2994
- makePipeline(kind) {
2995
- const handle = this.internalBookkeeper.register(kind);
2996
- const getBindGroupLayout = (_index) => this.internalBookkeeper.register("BindGroupLayout");
2997
- return Object.assign(handle, { getBindGroupLayout });
2998
- }
2999
- };
3000
- EMPTY_FEATURES = /* @__PURE__ */ new Set();
3001
- EMPTY_LIMITS = {};
3002
- NEVER = new Promise(() => {
3003
- });
3004
- RhiNullQueue = class {
3005
- writeBuffer(_buffer, _bufferOffset, _data, _dataOffset, _size) {
3006
- return ok(void 0);
3007
- }
3008
- writeTexture(_destination, _data, _dataLayout, _size) {
3009
- return ok(void 0);
3010
- }
3011
- copyExternalImageToTexture(_source, _destination, _copySize) {
3012
- return ok(void 0);
3013
- }
3014
- submit(_commandBuffers) {
3015
- return ok(void 0);
3016
- }
3017
- // forgeax-async-whitelist: dom-native — spec `GPUQueue.onSubmittedWorkDone`
3018
- // never rejects. The headless backend has no pending GPU work, so it resolves
3019
- // immediately (AC-12: read-back idioms must not hang).
3020
- onSubmittedWorkDone() {
3021
- return Promise.resolve(void 0);
3022
- }
3023
- };
3024
- RhiNullAdapter = class {
3025
- features = /* @__PURE__ */ new Set();
3026
- limits = {};
3027
- // forgeax-async-whitelist is not needed: this returns Promise<Result<...>>
3028
- // per the spec contract; never rejects.
3029
- requestDevice(_opts) {
3030
- const device = new RhiNullDevice(
3031
- new RhiNullQueue(),
3032
- (bookkeeper, dev) => new RhiNullCommandEncoder(bookkeeper, dev)
3033
- );
3034
- return Promise.resolve(ok(device));
3035
- }
3036
- };
3037
- RhiNullCanvasContext = class {
3038
- configure(_desc) {
3039
- return ok(void 0);
3040
- }
3041
- unconfigure() {
3042
- }
3043
- getConfiguration() {
3044
- return void 0;
3045
- }
3046
- getCurrentTexture() {
3047
- return ok({});
3048
- }
3049
- };
3050
- rhi = {
3051
- requestAdapter,
3052
- acquireCanvasContext,
3053
- createShaderModule
3054
- };
3011
+ execFileAsync3 = promisify(execFile);
3055
3012
  }
3056
3013
  });
3057
3014
  function createCliRhiDebugOperationContext() {
@@ -3059,7 +3016,7 @@ function createCliRhiDebugOperationContext() {
3059
3016
  captureFrame: async () => err(
3060
3017
  createRhiDebugError("capture-unavailable", {
3061
3018
  stage: "capture",
3062
- 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"
3063
3020
  })
3064
3021
  ),
3065
3022
  async readArtifact(artifact) {
@@ -3122,7 +3079,6 @@ function createCliRhiDebugOperationContext() {
3122
3079
  }
3123
3080
  var init_cli_context = __esm({
3124
3081
  "src/rhi-debug/cli-context.ts"() {
3125
- init_dist3();
3126
3082
  }
3127
3083
  });
3128
3084
  function createRhiDebugOperationContext(host) {
@@ -3427,7 +3383,7 @@ async function sdkInstallCommand(options) {
3427
3383
  try {
3428
3384
  staging = await mkdtemp(resolve(parent, `.${basename(root)}.forgeax-sdk-staging-`));
3429
3385
  download = await mkdtemp(resolve(parent, `.${basename(root)}.forgeax-sdk-download-`));
3430
- await execFileAsync3(
3386
+ await execFileAsync4(
3431
3387
  process.env.FORGEAX_NPM_CLIENT ?? "npm",
3432
3388
  [
3433
3389
  "install",
@@ -3460,7 +3416,15 @@ async function sdkInstallCommand(options) {
3460
3416
  await rename(resolve(staging, name), resolve(root, name));
3461
3417
  }
3462
3418
  }
3463
- 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
+ };
3464
3428
  } catch (cause) {
3465
3429
  return { ok: false, error: commandError(cause, "sdk-install-failed") };
3466
3430
  } finally {
@@ -3468,11 +3432,11 @@ async function sdkInstallCommand(options) {
3468
3432
  if (download !== void 0) await rm(download, { recursive: true, force: true });
3469
3433
  }
3470
3434
  }
3471
- var execFileAsync3;
3435
+ var execFileAsync4;
3472
3436
  var init_sdk_install = __esm({
3473
3437
  "src/sdk-install.ts"() {
3474
3438
  init_project();
3475
- execFileAsync3 = promisify(execFile);
3439
+ execFileAsync4 = promisify(execFile);
3476
3440
  }
3477
3441
  });
3478
3442
  async function shaderCheckCommand(options = {}) {
@@ -3543,6 +3507,21 @@ __export(commands_exports, {
3543
3507
  function runRhiDebugCommand(name, input, context) {
3544
3508
  return runRhiDebugOperation(name, input, context);
3545
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
+ }
3546
3525
  async function buildCommand(options = {}) {
3547
3526
  const facts = await readProjectFacts(options.root);
3548
3527
  if (!facts.ok) return facts;
@@ -3645,13 +3624,38 @@ async function devCommand(options = {}) {
3645
3624
  const facts = await readProjectFacts(options.root);
3646
3625
  if (!facts.ok) return facts;
3647
3626
  const previous = process.cwd();
3627
+ let server;
3648
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) };
3649
3633
  process.chdir(facts.value.root);
3650
- const server = await createServer(await createViteConfig(facts.value, "serve"));
3651
- await server.listen();
3634
+ server = await createServer2(
3635
+ await createViteConfig2(facts.value, "serve", "/", {
3636
+ server: vitePort
3637
+ })
3638
+ );
3639
+ await server.listen(vitePort.port);
3652
3640
  if (options.json !== true) server.printUrls();
3653
- 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
+ };
3654
3657
  } catch (cause) {
3658
+ await server?.close();
3655
3659
  process.chdir(previous);
3656
3660
  return { ok: false, error: commandError(cause, "dev-server-failed") };
3657
3661
  }
@@ -3661,15 +3665,31 @@ async function previewCommand(options = {}) {
3661
3665
  const verified = await verifyDist(resolve(root, "dist"));
3662
3666
  if (!verified.ok) return verified;
3663
3667
  try {
3668
+ const port = resolveProjectPort(options.port);
3664
3669
  const server = await preview({
3665
3670
  root,
3666
3671
  configFile: false,
3667
3672
  base: verified.value.base,
3668
- preview: { open: false, port: 0, strictPort: false },
3673
+ preview: { open: false, ...port },
3669
3674
  build: { outDir: resolve(root, "dist") }
3670
3675
  });
3671
3676
  if (options.json !== true) server.printUrls();
3672
- 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
+ };
3673
3693
  } catch (cause) {
3674
3694
  return { ok: false, error: commandError(cause, "preview-server-failed") };
3675
3695
  }
@@ -3719,6 +3739,7 @@ var init_commands = __esm({
3719
3739
  init_dist();
3720
3740
  init_host();
3721
3741
  init_project();
3742
+ init_types();
3722
3743
  init_assets();
3723
3744
  init_bootstrap_commands();
3724
3745
  init_plugin_authoring();
@@ -4091,7 +4112,7 @@ function hasStaticToolDeclarations(entry) {
4091
4112
  return Array.isArray(Reflect.get(entry.config, "tools"));
4092
4113
  }
4093
4114
  async function createViteModuleLoader(root) {
4094
- const server = await createServer({
4115
+ const server = await createServer$1({
4095
4116
  root,
4096
4117
  appType: "custom",
4097
4118
  configFile: false,
@@ -4293,7 +4314,7 @@ __export(contributions_exports, {
4293
4314
  createBuildContribution: () => createBuildContribution,
4294
4315
  createDefaultContributions: () => createDefaultContributions
4295
4316
  });
4296
- function commandFailure(error) {
4317
+ function commandFailure2(error) {
4297
4318
  return { ok: false, error: { ...error, detail: error.detail } };
4298
4319
  }
4299
4320
  function createBuildContribution(projectRoot2 = process.cwd()) {
@@ -4302,7 +4323,7 @@ function createBuildContribution(projectRoot2 = process.cwd()) {
4302
4323
  async (options) => {
4303
4324
  const { buildCommand: buildCommand2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
4304
4325
  const result = await buildCommand2({ ...options, root: options.root ?? projectRoot2 });
4305
- return result.ok ? result.value : commandFailure(result.error);
4326
+ return result.ok ? result.value : commandFailure2(result.error);
4306
4327
  }
4307
4328
  );
4308
4329
  }
@@ -4312,7 +4333,7 @@ function createAuthorContribution(projectRoot2 = process.cwd()) {
4312
4333
  async (options) => {
4313
4334
  const { pluginInstallCommand: pluginInstallCommand2 } = await Promise.resolve().then(() => (init_plugin_authoring(), plugin_authoring_exports));
4314
4335
  const result = await pluginInstallCommand2({ ...options, root: options.root ?? projectRoot2 });
4315
- return result.ok ? result.value : commandFailure(result.error);
4336
+ return result.ok ? result.value : commandFailure2(result.error);
4316
4337
  }
4317
4338
  );
4318
4339
  }
@@ -4348,6 +4369,36 @@ var init_runtime = __esm({
4348
4369
  }
4349
4370
  });
4350
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
+
4351
4402
  // src/cli.ts
4352
4403
  init_commands();
4353
4404
 
@@ -4547,13 +4598,16 @@ async function execCommand(options) {
4547
4598
  }
4548
4599
 
4549
4600
  // src/cli.ts
4550
- var args = process.argv.slice(2);
4601
+ init_types();
4602
+ var rawArgs = process.argv.slice(2);
4603
+ var args = [...rawArgs];
4551
4604
  var primary = args.shift();
4552
4605
  var nested = primary === "asset" || primary === "shader" || primary === "plugin" || primary === "skill" || primary === "sdk" ? args.shift() : void 0;
4553
4606
  var command = primary === "asset" || primary === "shader" || primary === "plugin" || primary === "skill" || primary === "sdk" ? `${primary}.${nested ?? ""}` : primary;
4554
4607
  var json = args.includes("--json");
4555
4608
  var dryRun = args.includes("--dry-run");
4556
4609
  var noInstall = args.includes("--no-install");
4610
+ var help = rawArgs.includes("--help") || rawArgs.includes("-h");
4557
4611
  function option(name) {
4558
4612
  const index = args.indexOf(name);
4559
4613
  return index < 0 ? void 0 : args[index + 1];
@@ -4570,6 +4624,7 @@ var optionValueIndexes = new Set(
4570
4624
  "--input",
4571
4625
  "--output",
4572
4626
  "--root",
4627
+ "--port",
4573
4628
  "--template",
4574
4629
  "--version",
4575
4630
  "--work-index"
@@ -4608,6 +4663,18 @@ var commands = [
4608
4663
  "describe",
4609
4664
  "exec"
4610
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
+ }
4611
4678
  async function run(value) {
4612
4679
  switch (value) {
4613
4680
  case "new": {
@@ -4624,8 +4691,10 @@ async function run(value) {
4624
4691
  return doctorCommand({ root: projectRoot});
4625
4692
  case "test":
4626
4693
  return testCommand({ root: projectRoot});
4627
- case "dev":
4628
- return devCommand({ root: projectRoot, json });
4694
+ case "dev": {
4695
+ const options = projectServerOptions();
4696
+ return options.ok ? devCommand(options.value) : options;
4697
+ }
4629
4698
  case "build": {
4630
4699
  const base = option("--base");
4631
4700
  const outDir = option("--out-dir");
@@ -4643,10 +4712,14 @@ async function run(value) {
4643
4712
  ...output === void 0 ? {} : { output }
4644
4713
  });
4645
4714
  }
4646
- case "preview":
4647
- return previewCommand({ root: projectRoot, json });
4648
- case "serve":
4649
- 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
+ }
4650
4723
  case "plugin.install": {
4651
4724
  const module = positionals[0];
4652
4725
  const id = option("--id");
@@ -4823,10 +4896,10 @@ async function run(value) {
4823
4896
  }
4824
4897
  }
4825
4898
  }
4826
- if (command === void 0 || !commands.includes(command)) {
4827
- process.stderr.write(
4828
- "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"
4829
- );
4899
+ if (help && primary !== "run") {
4900
+ process.stdout.write(renderForgeaxUsage());
4901
+ } else if (command === void 0 || !commands.includes(command)) {
4902
+ process.stderr.write(renderForgeaxUsage());
4830
4903
  process.exitCode = 2;
4831
4904
  } else {
4832
4905
  const stdoutWrite = process.stdout.write;
@@ -4846,9 +4919,23 @@ if (command === void 0 || !commands.includes(command)) {
4846
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 };
4847
4920
  if (json) stdoutWrite.call(process.stdout, `${JSON.stringify(envelope)}
4848
4921
  `);
4849
- 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}
4850
4926
  `);
4851
- else process.stderr.write(`[forgeax] ${result.error.code}: ${result.error.hint}
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}
4933
+ `);
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}
4852
4939
  `);
4853
4940
  if (!result.ok) process.exitCode = result.error.code === "cli-parse-error" ? 2 : 1;
4854
4941
  }