@vgai/cli 0.5.11 → 0.5.13

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 (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +308 -379
  3. package/package.json +7 -7
package/README.md CHANGED
@@ -74,7 +74,7 @@ vgai upgrade [--report] report engine drift vs. the project's engine pin
74
74
  present reports "not a scaffolded project" instead of
75
75
  dumping the whole template — see `vgai validate` instead)
76
76
  vgai validate [folder] in-process: validate vgai.project.json, check every declared
77
- entry/scene/entryHtml/bundleUrl file exists, print the engine
77
+ entry/contractShim file exists, print the engine
78
78
  pin (default: cwd) — no monorepo Vite/editor stack required
79
79
  Exit codes: 0 valid, 1 invalid manifest or missing file, 2 usage
80
80
  npm run vgai -- add deploy-vercel add project-owned Vercel scripts/config
package/dist/index.js CHANGED
@@ -413,6 +413,51 @@ var init_client = __esm({
413
413
  const data = await this.command({ type: "inspect" });
414
414
  return data.subject;
415
415
  }
416
+ /**
417
+ * The HIERARCHY PANEL's actual rendered row tree, as data.
418
+ *
419
+ * The same rows a human is looking at: the adapter's tree after the component
420
+ * marks fold implementation subtrees, after the internals reveal, after the
421
+ * document promotion, the child cap, the collapse state, the search filter
422
+ * and the selection scope. Works in play mode and edit mode alike — the
423
+ * answer reports which (`playState`, `activeViewportTab`), because a
424
+ * play-mode tree and an edit-mode tree come from different adapters.
425
+ *
426
+ * Deliberately NOT `status().entities`, which walks the raw adapter tree and
427
+ * therefore answers a different question: a panel defect is invisible in it.
428
+ *
429
+ * Each row carries `childCount` (what its caret opens), `internalChildCount`
430
+ * (what is folded behind "Reveal Internals") and `expandable` (whether the
431
+ * panel draws a caret at all) — so "this subtree exists but the UI offers no
432
+ * way to open it" is a readable fact rather than something only a human
433
+ * squinting at the panel can notice.
434
+ *
435
+ * Rejects, naming the panel, when no hierarchy panel is mounted: an empty
436
+ * tree would be a fabricated answer about a surface nobody is being shown.
437
+ */
438
+ async hierarchy() {
439
+ const data = await this.command({ type: "hierarchy" });
440
+ return data.hierarchy;
441
+ }
442
+ /** Write one editable path through the active Inspector's own IO. */
443
+ async setInspectionField(path, value) {
444
+ const data = await this.command({
445
+ type: "set-inspection-field",
446
+ path,
447
+ value
448
+ });
449
+ return data.subject;
450
+ }
451
+ /**
452
+ * Undo / redo one project transaction — the same queue the keyboard shortcut
453
+ * drives. `moved` is false when there was nothing left in that direction.
454
+ */
455
+ async undo() {
456
+ return this.command({ type: "undo" });
457
+ }
458
+ async redo() {
459
+ return this.command({ type: "redo" });
460
+ }
416
461
  /** Read the editor's actual current durable projection. */
417
462
  async currentView() {
418
463
  const data = await this.command({ type: "current-view" });
@@ -425,7 +470,30 @@ var init_client = __esm({
425
470
  ...size === void 0 ? {} : { size }
426
471
  });
427
472
  }
473
+ /**
474
+ * Read or drive the ACTIVE center document's own DOM — the scoped
475
+ * editor-chrome door, and the read/gesture half of the same subject
476
+ * {@link captureActiveDocument} photographs. NOT play-mode gated, and NOT
477
+ * page automation: a target outside the active document's container is
478
+ * refused by name. Design and scope contract:
479
+ * `packages/editor/src/editor-document-probe.ts`.
480
+ */
481
+ async documentProbe(step) {
482
+ return this.command({ type: "document-probe", step });
483
+ }
428
484
  // --- Display (set semantics) ---
485
+ /**
486
+ * The Game document's "Persist to game source" consent, over the relay —
487
+ * the same session-scoped switch the checkbox flips, refused for the same
488
+ * reasons (it answers with the server's own words when the base cannot be
489
+ * written). `recorder` names who accounts for the diff a write produces.
490
+ */
491
+ async setSourcePersistConsent(enabled) {
492
+ return this.command({
493
+ type: "set-source-persist-consent",
494
+ enabled
495
+ });
496
+ }
429
497
  async setGrid(enabled) {
430
498
  await this.command({ type: "set-grid", enabled });
431
499
  }
@@ -18296,6 +18364,9 @@ var init_inspection_operations = __esm({
18296
18364
  id: external_exports.string(),
18297
18365
  title: external_exports.string(),
18298
18366
  kindLabel: external_exports.string().optional(),
18367
+ note: external_exports.string().optional().describe(
18368
+ "The identity row\u2019s own line \u2014 for an ingest mount, where the object was CONSTRUCTED (`Source \xB7 game.js:271`), or the reason it has no such line."
18369
+ ),
18299
18370
  hint: external_exports.string().optional().describe("The quiet line a subject with nothing to edit carries."),
18300
18371
  presentation: external_exports.object({
18301
18372
  preferred: external_exports.string().describe("The surface\u2019s presentation affinity: card|column."),
@@ -18311,6 +18382,7 @@ var init_inspection_operations = __esm({
18311
18382
  disabled: external_exports.boolean().optional()
18312
18383
  })
18313
18384
  ),
18385
+ related: external_exports.array(external_exports.object({ id: external_exports.string(), title: external_exports.string() })).describe("Agent-visible counterparts of the inspector\u2019s related-document buttons."),
18314
18386
  sections: external_exports.array(InspectedSectionSchema)
18315
18387
  });
18316
18388
  NothingInspectedSchema = external_exports.object({ none: external_exports.literal(true).describe("There is no inspector showing.") }).describe("Nothing is being inspected.");
@@ -39603,36 +39675,12 @@ function rejectRemovedRootAdapterSpelling(value, ctx) {
39603
39675
  function rootMedium(root) {
39604
39676
  return typeof root.adapter === "string" ? root.adapter : root.adapter.surface;
39605
39677
  }
39606
- var GAME_MANIFEST_VERSION, IngestStrategySchema, TierSchema, REMOVED_SURFACE_SPELLING, REMOVED_BARE_ADAPTER_SPELLING, RootAdapterSchema, AdapterRootSchema, LearnKindSchema, LearnDifficultySchema, LearnMetadataSchema, GameManifestSchema;
39678
+ var GAME_MANIFEST_VERSION, REMOVED_SURFACE_SPELLING, REMOVED_BARE_ADAPTER_SPELLING, RootAdapterSchema, AdapterRootSchema, LearnKindSchema, LearnDifficultySchema, LearnMetadataSchema, GameManifestSchema;
39607
39679
  var init_schema = __esm({
39608
39680
  "../engine/src/manifest/schema.ts"() {
39609
39681
  "use strict";
39610
39682
  init_zod();
39611
39683
  GAME_MANIFEST_VERSION = 2;
39612
- IngestStrategySchema = external_exports.enum([
39613
- "shared",
39614
- // game imports host-served ESM three — full in-realm capture
39615
- "deduped",
39616
- // game source built by OUR pipeline, bundler dedupe — full capture
39617
- "iframe-reachable",
39618
- // prebuilt bundle in iframe, three reachable — cross-realm capture
39619
- "opaque-embed"
39620
- // prebuilt bundle, nothing reachable — host lifecycle only
39621
- ]);
39622
- TierSchema = external_exports.enum([
39623
- "first-party",
39624
- // vgai-native root: full editor, full loop control
39625
- "shared",
39626
- // = IngestStrategy 'shared' rung
39627
- "deduped",
39628
- // = IngestStrategy 'deduped' rung
39629
- "iframe-reachable",
39630
- // = IngestStrategy 'iframe-reachable' rung
39631
- "opaque-embed",
39632
- // = IngestStrategy 'opaque-embed' rung
39633
- "unsupported"
39634
- // this root does not function in this delivery context
39635
- ]);
39636
39684
  REMOVED_SURFACE_SPELLING = {
39637
39685
  threejs: "three",
39638
39686
  pixijs: "canvas",
@@ -39654,14 +39702,11 @@ var init_schema = __esm({
39654
39702
  external_exports.object({
39655
39703
  surface: external_exports.enum(["three", "canvas", "dom"]).describe("Native surface captured from the unmodified game"),
39656
39704
  ingest: external_exports.object({
39657
- strategy: IngestStrategySchema.describe(
39658
- "Capture mechanics rung this ingested root uses (\xA74) \u2014 also the root`s natural local capability tier"
39659
- ),
39660
- entryHtml: external_exports.string().optional().describe(
39661
- "Entry HTML path, project-relative. WIRED for opaque-embed (D-W2): the file`s bytes are read verbatim through the project-static route and hosted in a sandboxed iframe (embed-only, no scene introspection), for both canvas and three roots. Legal but NOT wired to a mount for iframe-reachable (a named error at resolve names this \u2014 use `bundleUrl` instead, the wired reachable route)."
39662
- ),
39663
39705
  contractShim: external_exports.string().optional().describe(
39664
- 'Project-relative path to a HOST-ADDED ES module that declares this game\'s `window.vgaiGame` contract (adapter/ingest/game-contract.ts) WITHOUT editing a vendored byte \u2014 the pristine-copy door. The host injects it as a `<script type="module">` into the assembled iframe document AFTER the game`s own importmap (moving a module load ahead of an import map invalidates the map) and BEFORE the game`s entry module, so deferred module scripts run it first. TIMING CONTRACT, which a shim author must obey: because it runs before the game`s entry, it MUST assign `window.vgaiGame` SYNCHRONOUSLY at top level \u2014 the host waits for that assignment and fails the mount by name if it never arrives. Anything that needs the game`s own modules must be LAZY: a dynamic `import()` INSIDE each verb/provider closure, resolved at call time. Importing a game module eagerly from the shim would evaluate it ahead of the game`s own entry and reorder its module side effects. Read by the vendored/self-hosted three route only (strategy `iframe-reachable` with `entryHtml`); declaring it elsewhere is a schema error rather than a silently dead field.'
39706
+ "Project-relative path to a HOST-ADDED ES module that declares this game's `window.vgaiGame` contract (adapter/ingest/game-contract.ts) WITHOUT editing a vendored byte \u2014 the pristine-copy door. The host imports it immediately BEFORE the game's own entry module, in the editor's realm. TIMING CONTRACT, which a shim author must obey: because it runs before the game`s entry, it MUST assign `window.vgaiGame` SYNCHRONOUSLY at top level. Anything that needs the game`s own modules must be LAZY: a dynamic `import()` INSIDE each verb/provider closure, resolved at call time. Importing a game module eagerly from the shim would evaluate it ahead of the game`s own entry and reorder its module side effects. Every such `import()` MUST take a STRING LITERAL, never a variable/table lookup. A variable specifier is not statically analyzable, so the dev server leaves it alone and it resolves at runtime against the shim`s own `/@fs/` url \u2014 while the game`s entry has had ITS relative imports rewritten to whatever url the server considers canonical for those files. Module identity is per-url, so the shim silently binds a SECOND, freshly-evaluated copy of the whole game and every provider reports a game that never started. Measured on three-descent: 285 segments through the game`s graph, 0 through the shim`s, and no error anywhere."
39707
+ ),
39708
+ dataWriter: external_exports.string().optional().describe(
39709
+ "Project-relative path to a HOST-ADDED ES module that writes this game's own DATA file back \u2014 the sibling of a source write for a game whose authorable truth is not source. Descent places its robots, powerups and hostages in binary records inside `descent.hog`, reaching no source literal, so an authored move of one is an edit to that file and nothing else can write it. The module declares two exports and the host knows nothing else about it: `dataFile` (a project-relative string \u2014 the ONE file this writer edits) and `planDataEdit(bytes, {record, property, baseline, next})`, which returns `{changed: true, bytes}` or `{changed: false, reason}` and may be async. It is loaded in the editor realm beside the game, so it may `import()` the game's own modules to resolve what only a running game knows (Descent's writer asks the game`s own `find_point_seg` which segment a moved object landed in, and refuses \"outside the mine\" rather than writing data the game would read back wrong). It NEVER fabricates: a record it cannot address, or a property it does not model, is a named refusal. An object is anchored to a record by carrying `userData.vgaiRecordIndex` \u2014 the game declares that identity itself (a recorded patch or a shim); the host never infers one."
39665
39710
  ),
39666
39711
  assets: external_exports.record(external_exports.string(), external_exports.string()).optional().describe(
39667
39712
  "Path-substring -> served-URL rewrites for this ingested game (IngestGame.assets today)"
@@ -39670,72 +39715,11 @@ var init_schema = __esm({
39670
39715
  "DOM API stub ids this ingested game requires to run headlessly/in-realm"
39671
39716
  ),
39672
39717
  captureTimeoutMs: external_exports.number().optional().describe(
39673
- "How long to wait for capture before REPORTING the degrade loudly \u2014 not how long to keep listening. On the self-hosted-three route the devtools subscription is permanent, so a renderer appearing later still upgrades a reported-embed-only mount in place; the host clamps this value so a long one cannot block the editor on a question the upgrade path answers anyway."
39674
- ),
39675
- // The six iframe-reachable-multi mount fields
39676
- // (`ingest-iframe-2d.ts`'s `IframeReachableMultiOpts`), legal
39677
- // ONLY when `strategy` is 'iframe-reachable' (enforced by the
39678
- // `.superRefine` below).
39679
- bundleUrl: external_exports.string().optional().describe(
39680
- "Absolute URL of the externalized built game bundle \u2014 the WIRED iframe-reachable mount route (iframe-reachable only), for BOTH canvas (Track P, ingest-iframe-2d.ts's mountIngestGame2DIframeReachableMulti) and three (D-W1, ingest-iframe-reachable-adapter.ts's mountIngestGameIframeReachableBundle). PRECONDITION: the bundle must externalize its root`s own runtime as a bare, un-rewritten import (`pixi.js` for canvas, `three` for three \u2014 `vgai bundle` does this for you) so it resolves through the iframe importmap to the HOST instance the capture trap is installed on; a bundle with an inlined runtime is unreachable and degrades to the embed-only floor. Exactly one of bundleUrl/entryHtml is required when strategy is iframe-reachable."
39681
- ),
39682
- baseHref: external_exports.string().optional().describe(
39683
- "Iframe <base href> so the game's relative asset URLs resolve (iframe-reachable only)."
39684
- ),
39685
- assetBaseUrl: external_exports.string().optional().describe(
39686
- "Host-side basePath forced onto the trapped runtime's own asset resolution \u2014 pixi's `Assets.init` for a canvas root, three's `DefaultLoadingManager.setURLModifier` for a three root (D-W1) \u2014 absolute, or root-relative (resolved against the editor page's own origin at mount time, ingest-mode.ts, so a static manifest never has to know the dev-server port) (iframe-reachable only \u2014 a document.write iframe keeps the parent window.location)."
39687
- ),
39688
- extraDeps: external_exports.array(external_exports.string()).optional().describe(
39689
- "Bare specifiers of this root's externalized deps beyond its own runtime (pixi.js/three \u2014 e.g. '@pixi/sound', 'gsap'). Resolved by the editor's host-namespace registry into module namespaces when known (D-P1 \u2014 the manifest carries data, not code); an unregistered specifier falls back to the open project's own node_modules (D-Z3), served as a verbatim iframe importmap URL. Throws a named error if neither resolves it (iframe-reachable only)."
39690
- ),
39691
- pixiModuleUrl: external_exports.string().optional().describe(
39692
- 'PIXI-ONLY: standalone matching-major pixi ESM URL to trap instead of the host pixi, for version-skewed games (e.g. a v6 game on a v8 host); this field\'s PRESENCE is how the skew is expressed (D-P2 \u2014 no separate strategy/tier for version skew) (iframe-reachable only). Declaring this on a `kind: "three"` root is schema-legal (this field is kind-agnostic here) but throws a NAMED not-supported error at resolve \u2014 three has no version-skew mount this wave (D-W7: a hypothetical `threeModuleUrl` is explicitly parked, no consumer exists).'
39693
- ),
39694
- bodyHtml: external_exports.string().optional().describe(
39695
- 'HTML injected into the iframe body before the game script boots (e.g. a `<div id="game-root">` the game expects to find) (iframe-reachable only).'
39718
+ "How long the host waits for this game's first captured frame before the mount FAILS by name. A game that needs a long boot (streamed world assets, a menu the player must clear) raises it; the default is 10s."
39696
39719
  )
39697
- }).describe(
39720
+ }).strict().describe(
39698
39721
  "Ingest adapter configuration for an unmodified game (a repo-vendored game or your own external folder)"
39699
- ).superRefine((ingest, ctx) => {
39700
- if (ingest.contractShim === void 0 || ingest.strategy === "iframe-reachable" && ingest.entryHtml !== void 0) {
39701
- return;
39702
- }
39703
- ctx.addIssue({
39704
- code: external_exports.ZodIssueCode.custom,
39705
- message: `\`contractShim\` is read ONLY by the self-hosted entry-document route, so it requires strategy 'iframe-reachable' together with \`entryHtml\` (got strategy "${ingest.strategy}"${ingest.entryHtml === void 0 ? " and no `entryHtml`" : ""}) \u2014 the host injects the shim module into the GAME'S OWN entry HTML (ingest-self-hosted-three-adapter.ts), and no other mount has such a document.`,
39706
- path: ["contractShim"]
39707
- });
39708
- }).superRefine((ingest, ctx) => {
39709
- const iframeOnlyFields = [
39710
- ["bundleUrl", ingest.bundleUrl !== void 0],
39711
- ["baseHref", ingest.baseHref !== void 0],
39712
- ["assetBaseUrl", ingest.assetBaseUrl !== void 0],
39713
- ["extraDeps", ingest.extraDeps !== void 0],
39714
- ["pixiModuleUrl", ingest.pixiModuleUrl !== void 0],
39715
- ["bodyHtml", ingest.bodyHtml !== void 0]
39716
- ];
39717
- if (ingest.strategy !== "iframe-reachable") {
39718
- for (const [key, present] of iframeOnlyFields) {
39719
- if (present) {
39720
- ctx.addIssue({
39721
- code: external_exports.ZodIssueCode.custom,
39722
- message: `\`${key}\` is only legal when strategy is 'iframe-reachable' (got "${ingest.strategy}") \u2014 bundleUrl/baseHref/assetBaseUrl/extraDeps/pixiModuleUrl/bodyHtml are the iframe-reachable-multi mount's options (canvas: ingest-iframe-2d.ts IframeReachableMultiOpts; three, D-W1: ingest-iframe-reachable-adapter.ts IframeReachableBundleOpts) and have no meaning under any other strategy.`,
39723
- path: [key]
39724
- });
39725
- }
39726
- }
39727
- return;
39728
- }
39729
- const hasBundleUrl = ingest.bundleUrl !== void 0;
39730
- const hasEntryHtml = ingest.entryHtml !== void 0;
39731
- if (hasBundleUrl === hasEntryHtml) {
39732
- ctx.addIssue({
39733
- code: external_exports.ZodIssueCode.custom,
39734
- message: `strategy 'iframe-reachable' requires EXACTLY ONE of \`bundleUrl\` (externalized multi-file bundle, the iframe-reachable-multi mount) or \`entryHtml\` (single-file/legacy iframe entry) \u2014 ${hasBundleUrl ? "both are present" : "neither is present"}.`,
39735
- path: hasBundleUrl ? ["bundleUrl"] : ["entryHtml"]
39736
- });
39737
- }
39738
- })
39722
+ )
39739
39723
  }).strict()
39740
39724
  ])
39741
39725
  ).describe(
@@ -39760,17 +39744,7 @@ var init_schema = __esm({
39760
39744
  "Marks this root as a DEV LAYER: the game's own dev GUI, not shipped game content. Four readers act on it. `mountManifestRoots` mounts it only when dev layers are enabled (`devLayersEnabled`, runtime/dev-layers.ts), so a production build carries no dev GUI; the host stacks every dev root ABOVE every non-dev root (a dev layer sits topmost, and as a DOM layer it steals no input from the game while closed); the play compositor EXCLUDES dev layers from a capture unless the capture asks for them; and the editor mounts a dev root as edit-time CHROME rather than authorable content (no hierarchy entry, no OID stamping, no JSX write-back, not selectable)."
39761
39745
  ),
39762
39746
  loop: external_exports.enum(["gated", "self-driven"]).default("gated").describe(
39763
- 'gated: host-driven loop (default). self-driven: this root drives its own loop (the "composited, unsynchronized" tier) \u2014 an axis independent of capability tier'
39764
- ),
39765
- capabilities: external_exports.object({
39766
- local: TierSchema.optional().describe(
39767
- "Declared capability tier when served via the local CLI (Vite pipeline); omitted -> derived (\xA74)"
39768
- ),
39769
- hosted: TierSchema.optional().describe(
39770
- "Declared capability tier when served hosted (no bundler: esbuild-wasm + externals only); omitted -> derived (\xA74)"
39771
- )
39772
- }).optional().describe(
39773
- "Per-delivery-context capability tier declarations (\xA74). Declaring above the adapter's ceiling for that context is an error; declaring at or below it (including `unsupported`) is legal."
39747
+ "gated: host-driven loop (default). self-driven: this root drives its own loop (composited, unsynchronized)"
39774
39748
  )
39775
39749
  }).strict();
39776
39750
  LearnKindSchema = external_exports.enum(["starter", "feature", "sample-game", "lesson-companion"]).describe(
@@ -39869,6 +39843,13 @@ var init_schema = __esm({
39869
39843
  width: external_exports.number().describe("Canvas width in pixels"),
39870
39844
  height: external_exports.number().describe("Canvas height in pixels")
39871
39845
  }).optional().describe("Canvas resolution"),
39846
+ rendering: external_exports.object({
39847
+ antialias: external_exports.boolean().describe(
39848
+ "Whether every three root is built with a multisampled drawing buffer. This is the ONE render property a world cannot declare for itself (world3d-react/renderer-config.ts): a WebGL context fixes its sample count at CREATION from this boolean, long before a world mounts, so it belongs to the PROJECT. The runtime reader is mount-manifest.ts, which threads it into createHostRenderer for each three root. WebGL exposes no sample COUNT \u2014 the implementation picks one (4x on every desktop browser measured), so a source engine that authored 8x/16x gets multisampling but not its exact count."
39849
+ )
39850
+ }).strict().optional().describe(
39851
+ "Construction-time renderer properties, which no world can declare after the fact. Everything a world CAN declare (tone mapping, output colour space, clear colour, shadow filter) lives on the world's own renderer config instead."
39852
+ ),
39872
39853
  debug: external_exports.object({
39873
39854
  allowInProduction: external_exports.boolean().describe(
39874
39855
  "Allow the ?vgai-debug=1 introspection bridge and debug-command invocation in production builds. Default false: the bridge only installs in dev builds. The runtime reader is the debug-bridge installer (D18)."
@@ -39925,6 +39906,11 @@ function checkAdapterEntryRules(root) {
39925
39906
  `Game manifest: root "${root.id}" uses the '${adapter}' adapter but declares no \`entry\` \u2014 a built-in adapter root is authored as an entry module.`
39926
39907
  );
39927
39908
  }
39909
+ if (typeof adapter === "object" && "ingest" in adapter && !root.entry) {
39910
+ throw new Error(
39911
+ `Game manifest: root "${root.id}" is an { ingest } root but declares no \`entry\` \u2014 the game's own entry module is what the host imports.`
39912
+ );
39913
+ }
39928
39914
  }
39929
39915
  function resolveAdapter(root) {
39930
39916
  const { adapter } = root;
@@ -39945,52 +39931,13 @@ function resolveAdapter(root) {
39945
39931
  type: "ingest",
39946
39932
  identity,
39947
39933
  surface: adapter.surface,
39948
- strategy: ingest.strategy,
39949
- entryHtml: ingest.entryHtml,
39950
39934
  contractShim: ingest.contractShim,
39935
+ dataWriter: ingest.dataWriter,
39951
39936
  assets: ingest.assets,
39952
39937
  domStubs: ingest.domStubs,
39953
- captureTimeoutMs: ingest.captureTimeoutMs,
39954
- bundleUrl: ingest.bundleUrl,
39955
- baseHref: ingest.baseHref,
39956
- assetBaseUrl: ingest.assetBaseUrl,
39957
- extraDeps: ingest.extraDeps,
39958
- pixiModuleUrl: ingest.pixiModuleUrl,
39959
- bodyHtml: ingest.bodyHtml
39938
+ captureTimeoutMs: ingest.captureTimeoutMs
39960
39939
  };
39961
39940
  }
39962
- function ceilingFor(adapter, context) {
39963
- if (typeof adapter === "string") return CAPABILITY_CEILINGS.builtin[context];
39964
- if (isModuleAdapter(adapter)) return CAPABILITY_CEILINGS.module[context];
39965
- return CAPABILITY_CEILINGS.ingest[adapter.ingest.strategy][context];
39966
- }
39967
- function resolveCapabilities(root) {
39968
- const contexts = ["local", "hosted"];
39969
- const result = {};
39970
- for (const context of contexts) {
39971
- const declared = root.capabilities?.[context];
39972
- if (declared !== void 0) {
39973
- const ceiling = ceilingFor(root.adapter, context);
39974
- if (TIER_RANK[declared] > TIER_RANK[ceiling]) {
39975
- throw new Error(
39976
- `Game manifest: root "${root.id}": declared capabilities.${context} tier "${declared}" exceeds the "${ceiling}" ceiling for this adapter in the ${context} context (\xA74). Under-claiming (including \`unsupported\`) is legal; over-claiming is not.`
39977
- );
39978
- }
39979
- result[context] = declared;
39980
- continue;
39981
- }
39982
- if (typeof root.adapter === "string") {
39983
- result[context] = "first-party";
39984
- } else if (isModuleAdapter(root.adapter)) {
39985
- throw new Error(
39986
- `Game manifest: root "${root.id}": { module } adapter requires an explicit capabilities.${context} declaration \u2014 a custom adapter's reach is not derivable (\xA74).`
39987
- );
39988
- } else {
39989
- result[context] = CAPABILITY_CEILINGS.ingest[root.adapter.ingest.strategy][context];
39990
- }
39991
- }
39992
- return result;
39993
- }
39994
39941
  function checkEngineVersionPin(version2) {
39995
39942
  if (!SEMVER_RE.test(version2)) {
39996
39943
  throw new Error(
@@ -40010,8 +39957,7 @@ function resolveRoot2(root) {
40010
39957
  zOrder: root.zOrder,
40011
39958
  pausable: root.pausable,
40012
39959
  dev: root.dev,
40013
- loop: root.loop,
40014
- capabilities: resolveCapabilities(root)
39960
+ loop: root.loop
40015
39961
  };
40016
39962
  }
40017
39963
  function loadGameManifest(raw) {
@@ -40044,6 +39990,7 @@ function loadGameManifest(raw) {
40044
39990
  roots,
40045
39991
  server: server2,
40046
39992
  resolution: manifest.resolution,
39993
+ rendering: manifest.rendering,
40047
39994
  authoring: manifest.authoring,
40048
39995
  debug: manifest.debug,
40049
39996
  determinism: manifest.determinism,
@@ -40051,29 +39998,11 @@ function loadGameManifest(raw) {
40051
39998
  learn: manifest.learn
40052
39999
  };
40053
40000
  }
40054
- var CAPABILITY_CEILINGS, TIER_RANK, DEFAULT_SERVER_MODULE, SEMVER_RE;
40001
+ var DEFAULT_SERVER_MODULE, SEMVER_RE;
40055
40002
  var init_load = __esm({
40056
40003
  "../engine/src/manifest/load.ts"() {
40057
40004
  "use strict";
40058
40005
  init_schema();
40059
- CAPABILITY_CEILINGS = {
40060
- builtin: { local: "first-party", hosted: "first-party" },
40061
- ingest: {
40062
- shared: { local: "shared", hosted: "shared" },
40063
- deduped: { local: "deduped", hosted: "unsupported" },
40064
- "iframe-reachable": { local: "iframe-reachable", hosted: "iframe-reachable" },
40065
- "opaque-embed": { local: "opaque-embed", hosted: "opaque-embed" }
40066
- },
40067
- module: { local: "first-party", hosted: "first-party" }
40068
- };
40069
- TIER_RANK = {
40070
- "first-party": 5,
40071
- shared: 4,
40072
- deduped: 4,
40073
- "iframe-reachable": 3,
40074
- "opaque-embed": 2,
40075
- unsupported: 1
40076
- };
40077
40006
  DEFAULT_SERVER_MODULE = "server/colyseus-setup.ts";
40078
40007
  SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?$/;
40079
40008
  }
@@ -42664,7 +42593,7 @@ function createIngestManifest(inspection, options) {
42664
42593
  roots: [
42665
42594
  {
42666
42595
  id: "game",
42667
- adapter: { surface, ingest: { strategy: "deduped" } },
42596
+ adapter: { surface, ingest: {} },
42668
42597
  entry
42669
42598
  }
42670
42599
  ]
@@ -304450,12 +304379,54 @@ var require_lib = __commonJS({
304450
304379
  }
304451
304380
  });
304452
304381
 
304382
+ // ../vgai-live/dist/editor-document.js
304383
+ var LiveEditorDocument;
304384
+ var init_editor_document = __esm({
304385
+ "../vgai-live/dist/editor-document.js"() {
304386
+ "use strict";
304387
+ LiveEditorDocument = class {
304388
+ #client;
304389
+ constructor(client) {
304390
+ this.#client = client;
304391
+ }
304392
+ /** Read matching elements inside the active document: tag, text, attributes,
304393
+ * value/checked/disabled and rect. `matched` is the total before `limit`. */
304394
+ async query(selector, options) {
304395
+ return this.#probe({
304396
+ action: "query",
304397
+ selector,
304398
+ ...options?.limit === void 0 ? {} : { limit: options.limit }
304399
+ });
304400
+ }
304401
+ /** A REAL pointer gesture (pointerdown/mousedown/focus/pointerup/mouseup/click)
304402
+ * — not `element.click()`, which a `pointerdown` listener never sees. */
304403
+ async click(selector, options) {
304404
+ return this.#probe({
304405
+ action: "click",
304406
+ selector,
304407
+ ...options?.index === void 0 ? {} : { index: options.index }
304408
+ });
304409
+ }
304410
+ /** A real keydown/keyup on the target, or on whatever inside the document has focus. */
304411
+ async key(key, options) {
304412
+ return this.#probe({ action: "key", key, ...options ?? {} });
304413
+ }
304414
+ /** A real `ClipboardEvent` carrying `text/plain` — the gesture nothing else
304415
+ * in the product can produce. */
304416
+ async paste(text, options) {
304417
+ return this.#probe({ action: "paste", text, ...options ?? {} });
304418
+ }
304419
+ #probe(step) {
304420
+ return this.#client.documentProbe(step);
304421
+ }
304422
+ };
304423
+ }
304424
+ });
304425
+
304453
304426
  // ../vgai-live/dist/editor.js
304454
304427
  function inferAssetKind(path) {
304455
304428
  if (path.endsWith(".prefab.json"))
304456
304429
  return "prefab";
304457
- if (path.endsWith(".mat.json"))
304458
- return "material";
304459
304430
  const dot = path.lastIndexOf(".");
304460
304431
  const ext = dot >= 0 ? path.slice(dot).toLowerCase() : "";
304461
304432
  return EXTENSION_KIND[ext] ?? "json";
@@ -304464,6 +304435,7 @@ var EXTENSION_KIND, LiveEditor;
304464
304435
  var init_editor2 = __esm({
304465
304436
  "../vgai-live/dist/editor.js"() {
304466
304437
  "use strict";
304438
+ init_editor_document();
304467
304439
  EXTENSION_KIND = {
304468
304440
  ".glb": "model",
304469
304441
  ".gltf": "model",
@@ -304473,10 +304445,15 @@ var init_editor2 = __esm({
304473
304445
  ".webp": "image",
304474
304446
  ".gif": "image",
304475
304447
  ".svg": "image",
304448
+ ".hdr": "image",
304449
+ ".exr": "image",
304476
304450
  ".mp3": "audio",
304477
304451
  ".ogg": "audio",
304478
304452
  ".wav": "audio",
304479
- ".flac": "audio"
304453
+ ".flac": "audio",
304454
+ ".glsl": "source",
304455
+ ".vert": "source",
304456
+ ".frag": "source"
304480
304457
  };
304481
304458
  LiveEditor = class {
304482
304459
  /** `#`-private, not `private`: `vgai eval --list` enumerates this object's
@@ -304484,8 +304461,18 @@ var init_editor2 = __esm({
304484
304461
  * raw `EditorClient` advertised beside them. See `./game-client/`'s
304485
304462
  * `client.ts` (GameClient's field block) for the full reasoning. */
304486
304463
  #client;
304464
+ /**
304465
+ * The ACTIVE center document's own DOM: read it, click it, key it, paste
304466
+ * into it. The one door onto editor chrome that is not play-mode gated, and
304467
+ * deliberately scoped to that document alone —
304468
+ * `packages/editor/src/editor-document-probe.ts` carries the design and the
304469
+ * refusal contract. Screenshotting the same subject is
304470
+ * {@link LiveEditor.captureActiveDocument}, not a fifth verb here.
304471
+ */
304472
+ document;
304487
304473
  constructor(client) {
304488
304474
  this.#client = client;
304475
+ this.document = new LiveEditorDocument(client);
304489
304476
  }
304490
304477
  /**
304491
304478
  * The active authoring adapter's persistence destination — where a save would
@@ -304637,6 +304624,23 @@ var init_editor2 = __esm({
304637
304624
  async shading(mode) {
304638
304625
  await this.#client.setShadingMode(mode);
304639
304626
  }
304627
+ /**
304628
+ * CONSENT to edits being written into the game's own source files, for this
304629
+ * session — the Game document's "Persist to game source" checkbox, reachable
304630
+ * from a script.
304631
+ *
304632
+ * Off by default every session, on purpose: it is a statement about what you
304633
+ * are doing right now, never a property of the game. With it off, an edit
304634
+ * lives on the running object and says so; with it on, an edit that can be
304635
+ * honestly anchored to the line that CREATED the object is written there,
304636
+ * and one that cannot still says so. Answers with the server's own phrase for
304637
+ * who records the resulting diff — your version control, or a vendored
304638
+ * game's own lock — and refuses, with the reason, where the checkbox is
304639
+ * disabled.
304640
+ */
304641
+ async persistToGameSource(on) {
304642
+ return this.#client.setSourcePersistConsent(on);
304643
+ }
304640
304644
  /**
304641
304645
  * READ the inspector, as data — the serialized inspection subject
304642
304646
  * (`editor.inspect()`; design: `docs/ARCHITECTURE-CORE.md` §Editor chrome,
@@ -304667,6 +304671,51 @@ var init_editor2 = __esm({
304667
304671
  async inspect() {
304668
304672
  return this.#client.inspect();
304669
304673
  }
304674
+ /**
304675
+ * READ the hierarchy panel, as data — the rows a human is looking at right
304676
+ * now, nested exactly as the panel nests them.
304677
+ *
304678
+ * The companion to {@link inspect}: that one answers "what IS the selected
304679
+ * thing", this one answers "what does the tree LOOK LIKE". It is the panel's
304680
+ * own output, not a fresh walk of the scene — the adapter's tree after the
304681
+ * component marks fold implementation subtrees (bones, particle renderers,
304682
+ * instanced pools), after the internals reveal, the document promotion, the
304683
+ * child cap, the collapse state, the search filter and the selection scope.
304684
+ *
304685
+ * Works in play mode and edit mode; the answer says which (`playState`,
304686
+ * `activeViewportTab`), because the two are different adapters and a tree
304687
+ * that looks wrong is very often the wrong adapter's tree.
304688
+ *
304689
+ * Prefer this over `status().entities`, which is deliberately a different
304690
+ * question — the RAW adapter tree, unprojected. A panel that renders the
304691
+ * wrong rows looks perfectly healthy in that facet.
304692
+ *
304693
+ * Each row carries `childCount` (what its caret opens), `internalChildCount`
304694
+ * (what is folded behind "Reveal Internals") and `expandable` (whether the
304695
+ * panel draws a caret at all), so "this subtree exists but nothing in the UI
304696
+ * opens it" is a fact you can read rather than one you have to notice.
304697
+ *
304698
+ * Rejects, naming the panel, when no hierarchy panel is mounted — an empty
304699
+ * tree would be a fabricated answer about a surface nobody is being shown.
304700
+ */
304701
+ async hierarchy() {
304702
+ return this.#client.hierarchy();
304703
+ }
304704
+ /**
304705
+ * Write one editable field from `inspect()` by its stable path, through the
304706
+ * same Inspector IO and persistence boundary the human control uses.
304707
+ */
304708
+ async setField(path, value) {
304709
+ return this.#client.setInspectionField(path, value);
304710
+ }
304711
+ /** Undo / redo one project transaction, through the session's own history
304712
+ * queue — the same one the keyboard shortcut drives. */
304713
+ async undo() {
304714
+ return this.#client.undo();
304715
+ }
304716
+ async redo() {
304717
+ return this.#client.redo();
304718
+ }
304670
304719
  /** Mirrors `vgai status` — the full live editor state as JSON. */
304671
304720
  async status() {
304672
304721
  return this.#client.getState();
@@ -306335,6 +306384,7 @@ __export(dist_exports, {
306335
306384
  HIDDEN_RECOVERY_STALL_POLLS: () => HIDDEN_RECOVERY_STALL_POLLS,
306336
306385
  HiddenRecoveryDriver: () => HiddenRecoveryDriver,
306337
306386
  LiveEditor: () => LiveEditor,
306387
+ LiveEditorDocument: () => LiveEditorDocument,
306338
306388
  LiveTools: () => LiveTools,
306339
306389
  PageTransport: () => PageTransport,
306340
306390
  RelayTransport: () => RelayTransport,
@@ -306408,6 +306458,7 @@ var init_dist = __esm({
306408
306458
  init_singleton();
306409
306459
  init_tools();
306410
306460
  init_editor2();
306461
+ init_editor_document();
306411
306462
  init_game();
306412
306463
  init_game_client();
306413
306464
  init_session();
@@ -307551,7 +307602,6 @@ function rewritePackageJson(targetDir, slug, engineDir, monoRoot) {
307551
307602
  function resolvePackageRoots() {
307552
307603
  return {
307553
307604
  engineRelPath: "node_modules/@vgai/engine",
307554
- p2pRelPath: "node_modules/@vgai/p2p-colyseus/",
307555
307605
  editorRelPath: "node_modules/@vgai/editor",
307556
307606
  liveRelPath: "node_modules/@vgai/live",
307557
307607
  sdkRelPath: "node_modules/@vgai/sdk",
@@ -307612,6 +307662,32 @@ function rewriteGameManifest(targetDir, name, slug, template, monoRoot) {
307612
307662
  writeJson(manifestPath, manifest);
307613
307663
  return manifest;
307614
307664
  }
307665
+ function repinEngineAfterInstall(targetDir) {
307666
+ const installed = readInstalledVersion(targetDir, "@vgai/engine");
307667
+ if (!installed) return null;
307668
+ const manifestPath = resolveManifestPath(targetDir);
307669
+ let manifest;
307670
+ try {
307671
+ manifest = JSON.parse(readFileSync12(manifestPath, "utf-8"));
307672
+ } catch {
307673
+ return null;
307674
+ }
307675
+ const engine = manifest["engine"] ?? {};
307676
+ const from = typeof engine["version"] === "string" ? engine["version"] : "";
307677
+ if (from === installed) return null;
307678
+ manifest["engine"] = { ...engine, version: installed };
307679
+ writeJson(manifestPath, manifest);
307680
+ const baselinePath = join14(targetDir, SCAFFOLD_BASELINE_RELATIVE_PATH);
307681
+ try {
307682
+ const baseline = JSON.parse(readFileSync12(baselinePath, "utf-8"));
307683
+ baseline.engineVersion = installed;
307684
+ baseline.files["vgai.project.json"] = hashFile(manifestPath);
307685
+ writeFileSync4(baselinePath, `${JSON.stringify(baseline, null, 2)}
307686
+ `, "utf-8");
307687
+ } catch {
307688
+ }
307689
+ return { from, to: installed };
307690
+ }
307615
307691
  var REACT_ONLY_PAGE_SOURCE = `export interface ReactGamePageProps {
307616
307692
  readonly lastInput: string;
307617
307693
  readonly title?: string;
@@ -307758,26 +307834,13 @@ function rewriteTsconfig(targetDir, engineRelPath, editorRelPath) {
307758
307834
  }
307759
307835
  writeJson(tsconfigPath, tsconfig);
307760
307836
  }
307761
- function rewriteViteConfig(targetDir, engineRelPath, p2pRelPath) {
307837
+ function rewriteViteConfig(targetDir) {
307762
307838
  const vitePath = join14(targetDir, "vite.config.ts");
307763
307839
  if (!existsSync11(vitePath)) return;
307764
- writeFileSync4(
307765
- vitePath,
307766
- rewriteViteConfigContent(readFileSync12(vitePath, "utf-8"), engineRelPath, p2pRelPath),
307767
- "utf-8"
307768
- );
307840
+ writeFileSync4(vitePath, rewriteViteConfigContent(readFileSync12(vitePath, "utf-8")), "utf-8");
307769
307841
  }
307770
- function rewriteViteConfigContent(content, engineRelPath, p2pRelPath) {
307771
- const enginePluginRoot = engineRelPath.startsWith(".") ? engineRelPath : `./${engineRelPath}`;
307772
- return ensureReactDedupe(
307773
- content.replace(
307774
- /'@engine':\s*path\.resolve\(__dirname,\s*['"][^'"]+['"]\)/,
307775
- `'@engine': path.resolve(__dirname, '${engineRelPath}/src')`
307776
- ).replace(/\.\.\/\.\.\/(?:packages\/)?p2p-colyseus\//g, p2pRelPath).replace(
307777
- /(['"])\.\.\/\.\.\/(?:packages\/)?engine\/src\/data\/vite-plugin-data\1/g,
307778
- `$1${enginePluginRoot}/src/data/vite-plugin-data$1`
307779
- )
307780
- );
307842
+ function rewriteViteConfigContent(content) {
307843
+ return ensureReactDedupe(content);
307781
307844
  }
307782
307845
  function ensureReactDedupe(content) {
307783
307846
  const REQUIRED = ["react", "react-dom"];
@@ -307797,32 +307860,6 @@ function ensureReactDedupe(content) {
307797
307860
  "resolve: {\n // react/react-dom dedupe (GH #123): see packages/editor/template/vite.config.ts's\n // matching comment for the dual-React-instance root cause this avoids.\n dedupe: ['react', 'react-dom'],"
307798
307861
  );
307799
307862
  }
307800
- function rewriteDataRefModule(targetDir, engineRelPath) {
307801
- const dataRefPath = join14(targetDir, "src", "data", "data-ref.ts");
307802
- if (!existsSync11(dataRefPath)) return;
307803
- writeFileSync4(
307804
- dataRefPath,
307805
- rewriteDataRefModuleContent(readFileSync12(dataRefPath, "utf-8"), engineRelPath),
307806
- "utf-8"
307807
- );
307808
- }
307809
- function rewriteDataRefModuleContent(content, engineRelPath) {
307810
- return content.replace(
307811
- // Match BOTH in-repo spellings, exactly like the sibling p2p-colyseus and
307812
- // vite-plugin-data rewrites above (`(?:packages\/)?`): the STARTER template
307813
- // (packages/editor/template/) spells this `../../../../engine/src/data/…`
307814
- // (already inside `packages/`), but an `examples/<id>/` project spells the
307815
- // identical on-disk target `../../../../packages/engine/src/data/…` (WITH
307816
- // the `packages/` segment). Without the optional `packages/` here a bare
307817
- // literal match no-oped on every data-trio EXAMPLE scaffold, leaving its
307818
- // data-ref.ts pointed at the in-checkout depth — which fails `tsc`/`vite
307819
- // build` the moment it's scaffolded into any external directory (E4
307820
- // AC-TPL-002; no prior CI externally-scaffolds a data-trio example, so it
307821
- // stayed latent). The p2p/vite-plugin rewrites already carried this guard.
307822
- /(['"])(?:\.\.\/)+(?:packages\/)?engine\/src\/data\/data-ref\1/,
307823
- `$1../../${engineRelPath}/src/data/data-ref$1`
307824
- );
307825
- }
307826
307863
  function rewriteIndexHtml(targetDir, name) {
307827
307864
  const htmlPath = join14(targetDir, "index.html");
307828
307865
  if (!existsSync11(htmlPath)) return;
@@ -307859,13 +307896,12 @@ function scaffoldProject(opts) {
307859
307896
  writeExampleReadme(targetDir, name, opts.exampleId);
307860
307897
  }
307861
307898
  if (opts.presentation) applyPresentation(targetDir, opts.presentation);
307862
- const { engineRelPath, p2pRelPath, editorRelPath } = resolvePackageRoots();
307899
+ const { engineRelPath, editorRelPath } = resolvePackageRoots();
307863
307900
  rewritePackageJson(targetDir, slug, engineDir, monoRoot);
307864
307901
  const manifest = rewriteGameManifest(targetDir, name, slug, template, monoRoot);
307865
307902
  rewriteTemplateVariantFiles(targetDir, template);
307866
307903
  rewriteTsconfig(targetDir, engineRelPath, editorRelPath);
307867
- rewriteViteConfig(targetDir, engineRelPath, p2pRelPath);
307868
- rewriteDataRefModule(targetDir, engineRelPath);
307904
+ rewriteViteConfig(targetDir);
307869
307905
  rewriteIndexHtml(targetDir, name);
307870
307906
  rewriteRoadmap(targetDir, name);
307871
307907
  if (hasCapabilityDistribution) initializeProjectCatalog(targetDir, catalogDir);
@@ -308058,15 +308094,6 @@ function checkAdapterWall(manifest) {
308058
308094
  }
308059
308095
  }
308060
308096
  }
308061
- function checkHostedTierWall(manifest) {
308062
- for (const world of manifest.roots) {
308063
- if (world.capabilities.hosted !== "first-party") {
308064
- throw new DeployError(
308065
- `vgai deploy: world "${world.id}" declares capabilities.hosted = "${world.capabilities.hosted}", below the required "first-party" tier \u2014 a static host IS a hosted delivery context (\xA71.B), and only first-party roots can ship there.`
308066
- );
308067
- }
308068
- }
308069
- }
308070
308097
  function runProjectBuild(projectDir, extraViteArgs) {
308071
308098
  const cmd = extraViteArgs ? `npm run build -- ${extraViteArgs}` : "npm run build";
308072
308099
  try {
@@ -308154,7 +308181,6 @@ async function deployProject(opts) {
308154
308181
  const { projectDir, reportOnly = false, wrangler } = opts;
308155
308182
  const manifest = loadManifestOrThrow(projectDir);
308156
308183
  checkAdapterWall(manifest);
308157
- checkHostedTierWall(manifest);
308158
308184
  const projectName = opts.projectName ?? defaultProjectName(manifest.name);
308159
308185
  const targetUrlShape = `${projectName}.pages.dev`;
308160
308186
  if (!reportOnly) {
@@ -309238,7 +309264,6 @@ async function packageItchZip(opts) {
309238
309264
  const { projectDir, reportOnly = false } = opts;
309239
309265
  const manifest = loadManifestOrThrow(projectDir);
309240
309266
  checkAdapterWall(manifest);
309241
- checkHostedTierWall(manifest);
309242
309267
  runProjectBuild(projectDir, "--base ./");
309243
309268
  const manifestPath = resolveManifestPath(projectDir);
309244
309269
  const staging = stageBuildOutput(projectDir, manifestPath);
@@ -309689,7 +309714,7 @@ function projectHasThreeRoot(projectDir) {
309689
309714
  }
309690
309715
  function reapplyScaffoldRewrites(scratchDir, projectDir, monoRoot, name, currentEngineVersion, targetPackageVersions) {
309691
309716
  const engineDir = resolveScaffoldPackageDir(monoRoot, join18("packages", "engine"), "@vgai/engine");
309692
- const { engineRelPath, p2pRelPath, editorRelPath, liveRelPath, sdkRelPath, editorSdkRelPath } = resolvePackageRoots();
309717
+ const { engineRelPath, editorRelPath, liveRelPath, sdkRelPath, editorSdkRelPath } = resolvePackageRoots();
309693
309718
  const targetEditorVersion = targetPackageVersions["@vgai/editor"] ?? readInstalledVersion(monoRoot, "@vgai/editor");
309694
309719
  const satelliteRelPaths = {
309695
309720
  "@vgai/live": liveRelPath,
@@ -309774,21 +309799,9 @@ function reapplyScaffoldRewrites(scratchDir, projectDir, monoRoot, name, current
309774
309799
  }
309775
309800
  const vitePath = join18(scratchDir, "vite.config.ts");
309776
309801
  if (existsSync14(vitePath)) {
309777
- const content = rewriteViteConfigContent(
309778
- readFileSync16(vitePath, "utf-8"),
309779
- engineRelPath,
309780
- p2pRelPath
309781
- );
309802
+ const content = rewriteViteConfigContent(readFileSync16(vitePath, "utf-8"));
309782
309803
  writeFileSync7(vitePath, content, "utf-8");
309783
309804
  }
309784
- const dataRefPath = join18(scratchDir, "src", "data", "data-ref.ts");
309785
- if (existsSync14(dataRefPath)) {
309786
- writeFileSync7(
309787
- dataRefPath,
309788
- rewriteDataRefModuleContent(readFileSync16(dataRefPath, "utf-8"), engineRelPath),
309789
- "utf-8"
309790
- );
309791
- }
309792
309805
  const htmlPath = join18(scratchDir, "index.html");
309793
309806
  if (existsSync14(htmlPath)) {
309794
309807
  const html = readFileSync16(htmlPath, "utf-8").replace(
@@ -310347,6 +310360,7 @@ function formatTabCensus(census) {
310347
310360
  census.heapUsedMB === null ? "heap n/a" : `heap ${census.heapUsedMB}MB${census.heapLimitMB === null ? "" : `/${census.heapLimitMB}MB`}`,
310348
310361
  `canvas ${census.canvasMB}MB in ${census.canvases}`
310349
310362
  ];
310363
+ if (census.mountEpochs !== void 0) parts.splice(1, 0, `mount epochs ${census.mountEpochs}`);
310350
310364
  if (census.textures !== void 0) parts.push(`tex ${census.textures}`);
310351
310365
  if (census.geometries !== void 0) parts.push(`geo ${census.geometries}`);
310352
310366
  if (census.programs !== void 0) parts.push(`prog ${census.programs}`);
@@ -321178,7 +321192,14 @@ function shipRoutesLine(projectRoot) {
321178
321192
  }
321179
321193
  return text.includes(STARTER_SENTINEL) ? "ship routes: not yet implemented (fine while the game is forming \u2014 see AGENTS.md)" : "ship routes: implemented";
321180
321194
  }
321181
- var GAMEPLAY_PATHS = ["src/components", "src/lib", "src/world.tsx", "src/hooks"];
321195
+ var GAMEPLAY_PATHS = [
321196
+ "src/components",
321197
+ "src/scenes",
321198
+ "src/prefabs",
321199
+ "src/lib",
321200
+ "src/world.tsx",
321201
+ "src/hooks"
321202
+ ];
321182
321203
  var STALE_AUTOPLAY_COMMIT_THRESHOLD = 5;
321183
321204
  function git2(cwd2, args2) {
321184
321205
  const result = spawnSync8("git", args2, { cwd: cwd2, encoding: "utf-8" });
@@ -321212,13 +321233,7 @@ function livePlaneLine(shape) {
321212
321233
  }
321213
321234
 
321214
321235
  // src/integrations.ts
321215
- import {
321216
- existsSync as existsSync22,
321217
- mkdirSync as mkdirSync11,
321218
- readFileSync as readFileSync25,
321219
- renameSync as renameSync3,
321220
- writeFileSync as writeFileSync9
321221
- } from "node:fs";
321236
+ import { existsSync as existsSync22, mkdirSync as mkdirSync11, readFileSync as readFileSync25, renameSync as renameSync3, writeFileSync as writeFileSync9 } from "node:fs";
321222
321237
  import { homedir as homedir6 } from "node:os";
321223
321238
  import { dirname as dirname13, join as join29 } from "node:path";
321224
321239
 
@@ -321285,9 +321300,7 @@ function parseHookStdin(raw) {
321285
321300
  return {};
321286
321301
  }
321287
321302
  function buildCallerSessionContext(opts) {
321288
- const lines = [
321289
- "vgai caller-session bridge (advisory, not a gate):"
321290
- ];
321303
+ const lines = ["vgai caller-session bridge (advisory, not a gate):"];
321291
321304
  if (opts.projectRoot) {
321292
321305
  lines.push(` project root: ${opts.projectRoot}`);
321293
321306
  } else {
@@ -321304,19 +321317,13 @@ function buildCallerSessionContext(opts) {
321304
321317
  }
321305
321318
  if (opts.projectRoot) {
321306
321319
  const canon = canonicalPath(opts.projectRoot);
321307
- const match = editors.find(
321308
- (s) => s.project !== null && canonicalPath(s.project) === canon
321309
- );
321320
+ const match = editors.find((s) => s.project !== null && canonicalPath(s.project) === canon);
321310
321321
  if (match) {
321311
- lines.push(
321312
- ` prefer this session for control commands: http://127.0.0.1:${match.port}`
321313
- );
321322
+ lines.push(` prefer this session for control commands: http://127.0.0.1:${match.port}`);
321314
321323
  }
321315
321324
  }
321316
321325
  }
321317
- lines.push(
321318
- " drive the game with `vgai eval`, never synthetic keys / wall-clock sleeps."
321319
- );
321326
+ lines.push(" drive the game with `vgai eval`, never synthetic keys / wall-clock sleeps.");
321320
321327
  return lines.join("\n");
321321
321328
  }
321322
321329
  function gatherLiveSessionNotes() {
@@ -321441,9 +321448,7 @@ function readIntegrationStatus(paths) {
321441
321448
  if (codex) {
321442
321449
  const hooks = codex["hooks"];
321443
321450
  if (typeof hooks === "object" && hooks !== null) {
321444
- codexSessionStartInstalled = hasVgaiCommandInHookList(
321445
- hooks["SessionStart"]
321446
- );
321451
+ codexSessionStartInstalled = hasVgaiCommandInHookList(hooks["SessionStart"]);
321447
321452
  legacyCodexPreToolUseBashGate = hasLegacyCodexPreToolUseBashGate(codex);
321448
321453
  }
321449
321454
  } else if (existsSync22(paths.codexHooksJson)) {
@@ -321453,14 +321458,10 @@ function readIntegrationStatus(paths) {
321453
321458
  if (claude) {
321454
321459
  const hooks = claude["hooks"];
321455
321460
  if (typeof hooks === "object" && hooks !== null) {
321456
- claudeSessionStartInstalled = hasVgaiCommandInHookList(
321457
- hooks["SessionStart"]
321458
- );
321461
+ claudeSessionStartInstalled = hasVgaiCommandInHookList(hooks["SessionStart"]);
321459
321462
  }
321460
321463
  } else if (existsSync22(paths.claudeSettingsJson)) {
321461
- notes.push(
321462
- `claude settings file exists but is not valid JSON: ${paths.claudeSettingsJson}`
321463
- );
321464
+ notes.push(`claude settings file exists but is not valid JSON: ${paths.claudeSettingsJson}`);
321464
321465
  }
321465
321466
  if (legacyCodexPreToolUseBashGate) {
321466
321467
  notes.push(
@@ -324162,9 +324163,6 @@ function detectUnmanagedReactRoots(source, file2) {
324162
324163
  }
324163
324164
 
324164
324165
  // src/validate.ts
324165
- function isServedRouteOrAbsoluteUrl(value) {
324166
- return /^[a-z][a-z0-9+.-]*:\/\//i.test(value) || value.startsWith("//") || value.startsWith("/project-game-static/");
324167
- }
324168
324166
  function checkFile(worldId, field, declaredPath, resolvedPath) {
324169
324167
  return { worldId, field, declaredPath, resolvedPath, exists: existsSync25(resolvedPath) };
324170
324168
  }
@@ -324175,16 +324173,9 @@ function collectFileChecks(folder, manifest) {
324175
324173
  checks.push(checkFile(world.id, "entry", world.entry, join35(folder, world.entry)));
324176
324174
  }
324177
324175
  if (world.adapter.type === "ingest") {
324178
- const { entryHtml, bundleUrl } = world.adapter;
324179
- if (entryHtml !== void 0) {
324180
- checks.push(
324181
- checkFile(world.id, "adapter.ingest.entryHtml", entryHtml, join35(folder, entryHtml))
324182
- );
324183
- }
324184
- if (bundleUrl !== void 0 && !isServedRouteOrAbsoluteUrl(bundleUrl)) {
324185
- checks.push(
324186
- checkFile(world.id, "adapter.ingest.bundleUrl", bundleUrl, join35(folder, bundleUrl))
324187
- );
324176
+ const shim = world.adapter.contractShim;
324177
+ if (shim !== void 0) {
324178
+ checks.push(checkFile(world.id, "adapter.ingest.contractShim", shim, join35(folder, shim)));
324188
324179
  }
324189
324180
  }
324190
324181
  }
@@ -324698,8 +324689,8 @@ function catalogDistributionDir() {
324698
324689
  );
324699
324690
  }
324700
324691
  function cliVersion() {
324701
- if ("0.5.11") {
324702
- return "0.5.11";
324692
+ if ("0.5.13") {
324693
+ return "0.5.13";
324703
324694
  }
324704
324695
  try {
324705
324696
  const pkg = JSON.parse(readFileSync32(join39(__dirname4, "..", "package.json"), "utf8"));
@@ -324718,7 +324709,7 @@ function bakedTargetVersions() {
324718
324709
  if (false)
324719
324710
  return void 0;
324720
324711
  try {
324721
- return JSON.parse('{"@vgai/engine":"0.5.11","@vgai/editor":"0.5.11","@vgai/p2p-colyseus":"0.5.11","@vgai/live":"0.5.11","@vgai/sdk":"0.5.11","@vgai/editor-sdk":"0.5.11","@vgai/cli":"0.5.11"}');
324712
+ return JSON.parse('{"@vgai/engine":"0.5.13","@vgai/editor":"0.5.13","@vgai/p2p-colyseus":"0.5.13","@vgai/live":"0.5.13","@vgai/sdk":"0.5.13","@vgai/editor-sdk":"0.5.13","@vgai/cli":"0.5.13"}');
324722
324713
  } catch {
324723
324714
  return void 0;
324724
324715
  }
@@ -324821,7 +324812,6 @@ Verify it:
324821
324812
  Ship it:
324822
324813
  npm run deploy The project's provider-native deployment script
324823
324814
  deploy [folder] Legacy Cloudflare/itch compatibility for existing projects
324824
- bundle <folder> Sidecar ESM bundle for an external pixi game
324825
324815
  render-cinematic Deterministic frame-addressed video export
324826
324816
 
324827
324817
  Bring in an existing web game:
@@ -324987,7 +324977,7 @@ Project:
324987
324977
  --to (that package, if named) > this CLI build's baked-in version
324988
324978
  > (in-repo only) this checkout's installed version.
324989
324979
  validate [folder] In-process: validate vgai.project.json, check every declared
324990
- entry/scene/entryHtml/bundleUrl file exists, print the engine pin
324980
+ entry/contractShim file exists, print the engine pin
324991
324981
  (default: cwd) \u2014 no monorepo/editor/Vite needed beyond this checkout
324992
324982
  Exit codes: 0 valid, 1 invalid manifest/files/React roots/design states, 2 usage
324993
324983
  Deployment is project-owned: add deploy-vercel or deploy-cloudflare, then
@@ -324996,15 +324986,6 @@ Project:
324996
324986
  --json Print a single machine-readable JSON report instead of text
324997
324987
  Exit codes: 0 pass, 1 conformance failure, 2 usage/environment error
324998
324988
  Requires a monorepo checkout (same precondition as \`edit\`)
324999
- bundle <folder> Build an external pixi game's iframe-reachable world as a sidecar
325000
- ESM bundle (pixi.js + declared extraDeps externalized), never
325001
- touching game source. Prints the vgai.project.json fields to add.
325002
- --write Patch vgai.project.json's bundleUrl in place instead of printing it
325003
- --entry <path> Build entry, folder-relative (default: probed from package.json)
325004
- --world <id> Which world to build (default: the manifest's one iframe-reachable
325005
- canvas world \u2014 an error if there are zero or several)
325006
- Exit codes: 0 built, 1 build failure, 2 usage error
325007
- Requires a monorepo checkout (same precondition as \`edit\`)
325008
324989
  doctor <folder> Headless mount of every declared world: static validation
325009
324990
  (manifest + declared files + engine pin), then a REAL headless
325010
324991
  editor session. Exactly one verdict per world \u2014
@@ -326042,11 +326023,7 @@ function renderValidateReport(report) {
326042
326023
  `${manifestPath}: OK \u2014 "${manifest.name}" v${manifest.version} (engine ${manifest.engine.version})`
326043
326024
  );
326044
326025
  for (const world of manifest.roots) {
326045
- const { adapter } = world;
326046
- const adapterLabel = adapter.type === "ingest" ? `${adapter.identity} (${adapter.strategy})` : adapter.identity;
326047
- lines.push(
326048
- ` root "${world.id}" surface=${world.surface} adapter=${adapterLabel} tiers={local: ${world.capabilities.local}, hosted: ${world.capabilities.hosted}}`
326049
- );
326026
+ lines.push(` root "${world.id}" surface=${world.surface} adapter=${world.adapter.identity}`);
326050
326027
  }
326051
326028
  lines.push("");
326052
326029
  const missing = fileChecks.filter((f) => !f.exists);
@@ -326448,6 +326425,24 @@ function describeEditorProvenance(packagedEntry, engineRoot) {
326448
326425
  }
326449
326426
  return `Editor: packaged @vgai/editor@${version2} (${editorPkgDir})`;
326450
326427
  }
326428
+ function assertEditorProjectInstalled(absProject, engineRoot = ENGINE_ROOT) {
326429
+ let realEngineRoot;
326430
+ try {
326431
+ realEngineRoot = realpathSync8(engineRoot);
326432
+ } catch {
326433
+ realEngineRoot = engineRoot;
326434
+ }
326435
+ const engineRootIsCheckout = existsSync28(
326436
+ join39(realEngineRoot, "packages", "editor", "package.json")
326437
+ );
326438
+ const projectIsInCheckout = absProject === realEngineRoot || absProject.startsWith(realEngineRoot + sep6);
326439
+ if (engineRootIsCheckout && projectIsInCheckout) return;
326440
+ if (existsSync28(join39(absProject, "node_modules"))) return;
326441
+ throw new Error(
326442
+ `Project dependencies are not installed in ${absProject}.
326443
+ Run \`npm install\` in that project, then run \`vgai edit\` again. vgai will not borrow React or editor packages from another checkout.`
326444
+ );
326445
+ }
326451
326446
  async function launchEditor(projectPath, opts = {}) {
326452
326447
  if (opts.noOpen === void 0 && process.env["VGAI_NO_OPEN"]) {
326453
326448
  opts = { ...opts, noOpen: true };
@@ -326457,6 +326452,7 @@ async function launchEditor(projectPath, opts = {}) {
326457
326452
  }
326458
326453
  const absProject = canonicalPath(resolve20(projectPath));
326459
326454
  discoverProject(absProject);
326455
+ assertEditorProjectInstalled(absProject);
326460
326456
  const portRequest = resolveEditorPortRequest(
326461
326457
  absProject,
326462
326458
  opts.portOverride,
@@ -326795,50 +326791,6 @@ No vgai.project.json in ${absFolder}`
326795
326791
  });
326796
326792
  process.exit(code);
326797
326793
  }
326798
- async function runBundle(folderArg, opts) {
326799
- const absFolder = resolve20(folderArg);
326800
- if (!existsSync28(absFolder) || !statSync12(absFolder).isDirectory()) {
326801
- console.error(
326802
- `Usage: vgai bundle <folder> [--write] [--entry <path>] [--world <id>]
326803
- Folder not found: ${absFolder}`
326804
- );
326805
- process.exit(2);
326806
- }
326807
- if (!hasManifest(absFolder)) {
326808
- console.error(
326809
- `Usage: vgai bundle <folder> [--write] [--entry <path>] [--world <id>]
326810
- No vgai.project.json in ${absFolder}`
326811
- );
326812
- process.exit(2);
326813
- }
326814
- const runnerScript = join39(ENGINE_ROOT, "packages/editor/scripts/run-bundle.ts");
326815
- if (!existsSync28(runnerScript)) {
326816
- throw new Error(`Bundle runner not found at ${runnerScript}. Is the engine installed?`);
326817
- }
326818
- const childArgs = [
326819
- "tsx",
326820
- runnerScript,
326821
- absFolder,
326822
- ...opts.write ? ["--write"] : [],
326823
- ...opts.entry !== void 0 ? ["--entry", opts.entry] : [],
326824
- ...opts.world !== void 0 ? ["--world", opts.world] : []
326825
- ];
326826
- const code = await new Promise((resolvePromise) => {
326827
- const child = spawn4("npx", childArgs, {
326828
- cwd: ENGINE_ROOT,
326829
- stdio: "inherit",
326830
- shell: true
326831
- });
326832
- child.on("error", (err2) => {
326833
- console.error(`Failed to start bundle runner: ${err2.message}`);
326834
- resolvePromise(1);
326835
- });
326836
- child.on("close", (exitCode) => {
326837
- resolvePromise(exitCode ?? 1);
326838
- });
326839
- });
326840
- process.exit(code);
326841
- }
326842
326794
  var DOCTOR_USAGE = "Usage: vgai doctor <folder> [--json] [--port <n>] [--timeout <ms>]";
326843
326795
  async function runDoctor(folderArg, opts) {
326844
326796
  const absFolder = resolve20(folderArg);
@@ -327714,7 +327666,6 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
327714
327666
  "screenshot",
327715
327667
  "eval",
327716
327668
  "conformance",
327717
- "bundle",
327718
327669
  "doctor",
327719
327670
  "render-cinematic",
327720
327671
  "perf",
@@ -328212,6 +328163,10 @@ Unrecognized option or invalid project name: ${name}`);
328212
328163
  throw new Error(`npm install exited with code ${exitCode ?? "unknown"}`);
328213
328164
  cacheDependencies(targetDir);
328214
328165
  }
328166
+ const repin = repinEngineAfterInstall(targetDir);
328167
+ if (repin) {
328168
+ console.log(`Engine pin updated ${repin.from} -> ${repin.to} (the project's installed @vgai/engine).`);
328169
+ }
328215
328170
  } catch {
328216
328171
  console.error("npm install failed. The editor was not started; fix the install and retry.");
328217
328172
  process.exitCode = 1;
@@ -328324,7 +328279,7 @@ ${subcommand === "list" ? `Unknown option: ${unknown2[0]}` : `Unknown subcommand
328324
328279
  console.log(`Usage: vgai validate [folder]
328325
328280
 
328326
328281
  In-process: validate vgai.project.json, check every declared
328327
- entry/scene/entryHtml/bundleUrl file exists, print the engine pin (default: cwd)
328282
+ entry/contractShim file exists, print the engine pin (default: cwd)
328328
328283
  \u2014 no monorepo/editor/Vite needed beyond this checkout.
328329
328284
 
328330
328285
  Exit codes: 0 valid, 1 invalid manifest/files/React roots/design states, 2 usage`);
@@ -328911,33 +328866,6 @@ Choose exactly one target: project, --port/--url, --all, or --everywhere.`
328911
328866
  await runConformance(positional[0], jsonMode);
328912
328867
  break;
328913
328868
  }
328914
- case "bundle": {
328915
- if (hasHelpFlag(args)) {
328916
- console.log("Usage: vgai bundle <folder> [--write] [--entry <path>] [--world <id>]");
328917
- break;
328918
- }
328919
- const write = args.includes("--write");
328920
- const entryIdx = args.indexOf("--entry");
328921
- const entry = entryIdx >= 0 ? args[entryIdx + 1] : void 0;
328922
- const worldIdx = args.indexOf("--world");
328923
- const worldId = worldIdx >= 0 ? args[worldIdx + 1] : void 0;
328924
- const positional = [];
328925
- for (let i = 1; i < args.length; i++) {
328926
- const a = args[i];
328927
- if (a === "--write") continue;
328928
- if (a === "--entry" || a === "--world") {
328929
- i++;
328930
- continue;
328931
- }
328932
- positional.push(a);
328933
- }
328934
- if (!positional[0]) {
328935
- console.error("Usage: vgai bundle <folder> [--write] [--entry <path>] [--world <id>]");
328936
- process.exit(2);
328937
- }
328938
- await runBundle(positional[0], { write, entry, world: worldId });
328939
- break;
328940
- }
328941
328869
  case "doctor": {
328942
328870
  if (hasHelpFlag(args)) {
328943
328871
  console.log(DOCTOR_USAGE);
@@ -329435,6 +329363,7 @@ if (process.argv[1] && import.meta.url === pathToFileURL2(realpathOrSelf(process
329435
329363
  export {
329436
329364
  BRIDGE_SCREENSHOT_STALE,
329437
329365
  HIDDEN_FRAME_NOTICE,
329366
+ assertEditorProjectInstalled,
329438
329367
  authoringWarningsWarning,
329439
329368
  autoAddCapabilityForTool,
329440
329369
  autoLaunchEditorAfterCreate,
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@vgai/cli",
3
3
  "author": "Volter AI, Inc.",
4
4
  "license": "Apache-2.0",
5
- "version": "0.5.11",
5
+ "version": "0.5.13",
6
6
  "description": "Create, open, control, validate, and playtest VGAI game projects.",
7
7
  "keywords": [
8
8
  "game-engine",
@@ -39,12 +39,12 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@oclif/core": "^4.11.14",
42
- "@vgai/editor": "0.5.11",
43
- "@vgai/editor-sdk": "0.5.11",
44
- "@vgai/engine": "0.5.11",
45
- "@vgai/live": "0.5.11",
46
- "@vgai/p2p-colyseus": "0.5.11",
47
- "@vgai/sdk": "0.5.11",
42
+ "@vgai/editor": "0.5.13",
43
+ "@vgai/editor-sdk": "0.5.13",
44
+ "@vgai/engine": "0.5.13",
45
+ "@vgai/live": "0.5.13",
46
+ "@vgai/p2p-colyseus": "0.5.13",
47
+ "@vgai/sdk": "0.5.13",
48
48
  "ink": "^7.1.0",
49
49
  "playwright": "^1.58.2",
50
50
  "react": "^19.2.4",