@vgai/live 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@vgai/live",
3
3
  "author": "Volter AI, Inc.",
4
4
  "license": "Apache-2.0",
5
- "version": "0.4.0",
5
+ "version": "0.4.1",
6
6
  "type": "module",
7
7
  "repository": {
8
8
  "type": "git",
@@ -30,9 +30,9 @@
30
30
  "build": "tsc -p tsconfig.build.json"
31
31
  },
32
32
  "dependencies": {
33
- "@vgai/editor-sdk": "0.4.0",
34
- "@vgai/probe": "0.4.0",
35
- "@vgai/sdk": "0.4.0"
33
+ "@vgai/editor-sdk": "0.4.1",
34
+ "@vgai/probe": "0.4.1",
35
+ "@vgai/sdk": "0.4.1"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@playwright/test": ">=1.58.2 <2"
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `@vgai/live` — `{ editor, game, page }` over the session wire
2
+ * `@vgai/live` — `{ editor, game, page, tools }` over the session wire
3
3
  * (docs/SHARED-SESSION-SPEC.md, Wave 2). Editor control consolidates HERE
4
4
  * from the ~19 `vgai` CLI verbs: a plain node/tsx script gets
5
5
  * `import { editor, game, page } from '@vgai/live'` instead of shelling out
@@ -45,6 +45,7 @@ import { createGameClient } from './game.js';
45
45
  import { lazyChainProxy } from './lazy-proxy.js';
46
46
  import { type ResolvedSession, resolveSession, type SessionResolutionDeps } from './session.js';
47
47
  import { createLazySession } from './singleton.js';
48
+ import { LiveTools } from './tools.js';
48
49
 
49
50
  export type { EditorClient } from '@vgai/editor-sdk';
50
51
  export type { GameClient } from '@vgai/probe';
@@ -59,6 +60,7 @@ export type {
59
60
  export { findProjectRootFrom, resolveSession } from './session.js';
60
61
  export type { LazySession } from './singleton.js';
61
62
  export { createLazySession } from './singleton.js';
63
+ export { LiveTools } from './tools.js';
62
64
 
63
65
  /** A `game.page(step)`-shaped call — see this module's doc comment for the closure-capture limitation. */
64
66
  export type PageStep = GameClient['page'];
@@ -68,6 +70,8 @@ export interface LiveSession {
68
70
  game: GameClient;
69
71
  /** `GameClient.page` bound to `game` — see this module's own doc comment for the wire limitation. */
70
72
  page: PageStep;
73
+ /** Registered project callables: enumerate, inspect, and invoke. */
74
+ tools: LiveTools;
71
75
  /** The resolved session this is bound to — useful for logging/debugging which port/project a script attached to. */
72
76
  session: ResolvedSession;
73
77
  }
@@ -87,7 +91,8 @@ export async function connect(
87
91
  const editor = new LiveEditor(client, resolved.projectRoot);
88
92
  const game = createGameClient(resolved.port);
89
93
  const page: PageStep = (step) => game.page(step);
90
- return { editor, game, page, session: resolved };
94
+ const tools = new LiveTools(client);
95
+ return { editor, game, page, tools, session: resolved };
91
96
  }
92
97
 
93
98
  // ---------------------------------------------------------------------------
@@ -113,3 +118,6 @@ export const game: GameClient = lazyChainProxy<GameClient>(() =>
113
118
  export const page: PageStep = lazyChainProxy<PageStep>(() =>
114
119
  lazySession.ensure().then((s) => s.page),
115
120
  );
121
+ export const tools: LiveTools = lazyChainProxy<LiveTools>(() =>
122
+ lazySession.ensure().then((s) => s.tools),
123
+ );
package/src/tools.ts ADDED
@@ -0,0 +1,32 @@
1
+ /** Registered project tools over the current shared editor session. */
2
+
3
+ import type {
4
+ EditorClient,
5
+ ProjectToolCatalog,
6
+ ProjectToolCatalogEntry,
7
+ ProjectToolOutcome,
8
+ } from '@vgai/editor-sdk';
9
+
10
+ export class LiveTools {
11
+ constructor(private readonly client: EditorClient) {}
12
+
13
+ /** Enumerate the exact `package.json#vgai.tools` catalog without executing it. */
14
+ async list(): Promise<ProjectToolCatalog> {
15
+ return this.client.listProjectTools();
16
+ }
17
+
18
+ /** Return one tool's discoverable metadata, or `null` when it is not registered. */
19
+ async describe(name: string): Promise<ProjectToolCatalogEntry | null> {
20
+ const catalog = await this.list();
21
+ return catalog.tools.find((tool) => tool.name === name) ?? null;
22
+ }
23
+
24
+ /** Invoke the same validated callable used by the editor and CLI. */
25
+ async call(
26
+ name: string,
27
+ input: unknown = {},
28
+ options: { confirm?: boolean } = {},
29
+ ): Promise<ProjectToolOutcome> {
30
+ return this.client.runProjectTool(name, input, options);
31
+ }
32
+ }
package/dist/editor.d.ts DELETED
@@ -1,100 +0,0 @@
1
- /**
2
- * `LiveEditor` — the editor-control half of `@vgai/live`'s `{ editor, game,
3
- * page }` (SHARED-SESSION-SPEC.md, Wave 2). Methods are named after the
4
- * ACTION, not the CLI flag spelling (e.g. `select('all')` rather than a
5
- * separate `selectAll`, `showPanel('viewport-game')` rather than
6
- * `vgai show viewport game`'s two-token shape) — see each method's own doc
7
- * comment for the exact CLI verb / `EditorClient` call it mirrors.
8
- *
9
- * Every method reuses `@vgai/editor-sdk`'s `EditorClient` — this module
10
- * never hand-rolls a `fetch` to `/__editor/command` itself. The ONE
11
- * exception is `applyDiff`, which is FILE mode (writes the scene file
12
- * directly), not a live wire command — see its own doc comment.
13
- */
14
- import type { AssetKind, AssetPreviewCapture, AssetPreviewOptions, EditorClient, EditorState, ShadingMode, ViewPreset, ViewportCapture } from '@vgai/editor-sdk';
15
- /** `vgai show <...>`'s four sub-verbs folded into one action name — see `showPanel`. */
16
- export type PanelName = 'viewport-scene' | 'viewport-game' | 'inspector' | 'console' | 'build';
17
- /** Mirrors `@vgai/sdk`'s `project.scene.apply` operation input — the same shape `vgai apply-diff` builds from its `<scene> <patch>` file arguments. `patch` is a `SceneDiff` JSON object (validated by the operation's own Zod schema — a malformed patch throws, never silently no-ops). */
18
- export interface ApplyDiffInput {
19
- /** Project-relative path to the `.vscn.json` scene file to modify. */
20
- scenePath: string;
21
- /** A `SceneDiff`-shaped patch object. */
22
- patch: unknown;
23
- /** Validate + report only — never write to disk. Default `false`. */
24
- dryRun?: boolean;
25
- /** Refuse a stale write when the file's content hash no longer matches (optimistic concurrency) — omit to skip the check. */
26
- baseHash?: string;
27
- }
28
- /** The `project.scene.apply` operation's result shape. */
29
- export interface ApplyDiffResult {
30
- dryRun: boolean;
31
- written: boolean;
32
- filesChanged: string[];
33
- before: {
34
- scene: unknown;
35
- };
36
- after: {
37
- scene: unknown;
38
- };
39
- }
40
- /**
41
- * Lightweight extension-based `AssetKind` guess for `openAsset`'s optional
42
- * `kind` argument. Deliberately independent of (not shared with) the
43
- * editor's own `AssetBrowser.tsx#getAssetKind` — that function is a private,
44
- * React-component-local helper of a package `@vgai/live` has no dependency
45
- * on. Callers with an unusual extension can always pass `kind` explicitly.
46
- */
47
- export declare function inferAssetKind(path: string): AssetKind;
48
- export declare class LiveEditor {
49
- private readonly client;
50
- /** Absolute project root this session is bound to — used only by `applyDiff` (FILE mode; every other method goes over the wire and never needs it). */
51
- private readonly projectRoot;
52
- constructor(client: EditorClient,
53
- /** Absolute project root this session is bound to — used only by `applyDiff` (FILE mode; every other method goes over the wire and never needs it). */
54
- projectRoot: string);
55
- /**
56
- * No `path` -> the currently open scene's save path (`status().savePath`).
57
- * `path` given -> opens it (`EditorClient.openScene` — the same wire call
58
- * `vgai scene <path>` sends), and the given path is returned back.
59
- */
60
- scene(path?: string): Promise<string | null>;
61
- /**
62
- * FILE mode — writes the scene file directly. NOT a live wire command:
63
- * there is no live-editor wire op for this yet (`vgai apply-diff`'s own
64
- * module doc names the identical gap: "LIVE MODE — NOT BUILT ... route the
65
- * patch through a RUNNING editor"). Reuses the exact same engine
66
- * machinery `vgai apply-diff` uses, through `@vgai/sdk`'s
67
- * `project.scene.apply` operation (the operation-registry's own
68
- * projection of that logic) — never hand-rolled here.
69
- */
70
- applyDiff(input: ApplyDiffInput): Promise<ApplyDiffResult>;
71
- play(opts?: {
72
- seed?: number;
73
- }): Promise<void>;
74
- stop(): Promise<void>;
75
- pause(): Promise<void>;
76
- resume(): Promise<void>;
77
- /** Advance `n` frames (default 1) — `EditorClient.step()` sent `n` times, mirroring `vgai step` run repeatedly. */
78
- step(n?: number): Promise<void>;
79
- /** `'all'` -> `EditorClient.selectAll()` (mirrors `vgai select --all`); otherwise `EditorClient.select(id)` (mirrors `vgai select <entityId>`). */
80
- select(id: string | 'all'): Promise<void>;
81
- /** Mirrors `vgai deselect`. */
82
- deselect(): Promise<void>;
83
- /** No `id` -> focus the current selection (mirrors bare `vgai focus`); `id` given -> focus that entity. */
84
- focus(id?: string): Promise<void>;
85
- view(preset: ViewPreset): Promise<void>;
86
- /** `vgai show <viewport <scene|game>|inspector|console|build>`'s four sub-verbs, folded into one action name. */
87
- showPanel(name: PanelName): Promise<void>;
88
- /** `kind` inferred from `path`'s extension when omitted (`inferAssetKind`) — pass it explicitly to override. */
89
- openAsset(path: string, kind?: AssetKind): Promise<void>;
90
- /** Captures the editor's native Asset Lab four-view preview for `path` (a project-relative asset path). */
91
- assetPreview(path: string, options?: AssetPreviewOptions): Promise<AssetPreviewCapture>;
92
- grid(on: boolean): Promise<void>;
93
- helpers(on: boolean): Promise<void>;
94
- stats(on: boolean): Promise<void>;
95
- shading(mode: ShadingMode): Promise<void>;
96
- /** Mirrors `vgai status` — the full live editor state as JSON. */
97
- status(): Promise<EditorState>;
98
- /** A live viewport PNG (`EditorClient.captureViewport`) — no direct CLI verb exists; this is the closest wire read. */
99
- screenshot(size?: number): Promise<ViewportCapture>;
100
- }
package/dist/editor.js DELETED
@@ -1,175 +0,0 @@
1
- /**
2
- * `LiveEditor` — the editor-control half of `@vgai/live`'s `{ editor, game,
3
- * page }` (SHARED-SESSION-SPEC.md, Wave 2). Methods are named after the
4
- * ACTION, not the CLI flag spelling (e.g. `select('all')` rather than a
5
- * separate `selectAll`, `showPanel('viewport-game')` rather than
6
- * `vgai show viewport game`'s two-token shape) — see each method's own doc
7
- * comment for the exact CLI verb / `EditorClient` call it mirrors.
8
- *
9
- * Every method reuses `@vgai/editor-sdk`'s `EditorClient` — this module
10
- * never hand-rolls a `fetch` to `/__editor/command` itself. The ONE
11
- * exception is `applyDiff`, which is FILE mode (writes the scene file
12
- * directly), not a live wire command — see its own doc comment.
13
- */
14
- import { operations } from '@vgai/sdk';
15
- const EXTENSION_KIND = {
16
- '.glb': 'model',
17
- '.gltf': 'model',
18
- '.png': 'image',
19
- '.jpg': 'image',
20
- '.jpeg': 'image',
21
- '.webp': 'image',
22
- '.gif': 'image',
23
- '.mp3': 'audio',
24
- '.ogg': 'audio',
25
- '.wav': 'audio',
26
- '.flac': 'audio',
27
- };
28
- /**
29
- * Lightweight extension-based `AssetKind` guess for `openAsset`'s optional
30
- * `kind` argument. Deliberately independent of (not shared with) the
31
- * editor's own `AssetBrowser.tsx#getAssetKind` — that function is a private,
32
- * React-component-local helper of a package `@vgai/live` has no dependency
33
- * on. Callers with an unusual extension can always pass `kind` explicitly.
34
- */
35
- export function inferAssetKind(path) {
36
- if (path.endsWith('.vscn.json'))
37
- return 'scene';
38
- if (path.endsWith('.prefab.json'))
39
- return 'prefab';
40
- if (path.endsWith('.mat.json'))
41
- return 'material';
42
- const dot = path.lastIndexOf('.');
43
- const ext = dot >= 0 ? path.slice(dot).toLowerCase() : '';
44
- return EXTENSION_KIND[ext] ?? 'json';
45
- }
46
- export class LiveEditor {
47
- client;
48
- projectRoot;
49
- constructor(client,
50
- /** Absolute project root this session is bound to — used only by `applyDiff` (FILE mode; every other method goes over the wire and never needs it). */
51
- projectRoot) {
52
- this.client = client;
53
- this.projectRoot = projectRoot;
54
- }
55
- /**
56
- * No `path` -> the currently open scene's save path (`status().savePath`).
57
- * `path` given -> opens it (`EditorClient.openScene` — the same wire call
58
- * `vgai scene <path>` sends), and the given path is returned back.
59
- */
60
- async scene(path) {
61
- if (path !== undefined) {
62
- await this.client.openScene(path);
63
- return path;
64
- }
65
- const state = await this.client.getState();
66
- return state.savePath;
67
- }
68
- /**
69
- * FILE mode — writes the scene file directly. NOT a live wire command:
70
- * there is no live-editor wire op for this yet (`vgai apply-diff`'s own
71
- * module doc names the identical gap: "LIVE MODE — NOT BUILT ... route the
72
- * patch through a RUNNING editor"). Reuses the exact same engine
73
- * machinery `vgai apply-diff` uses, through `@vgai/sdk`'s
74
- * `project.scene.apply` operation (the operation-registry's own
75
- * projection of that logic) — never hand-rolled here.
76
- */
77
- async applyDiff(input) {
78
- const outcome = await operations.dispatch('project.scene.apply', input, {
79
- projectRoot: this.projectRoot,
80
- });
81
- if (!outcome.ok) {
82
- throw new Error(`@vgai/live: applyDiff failed [${outcome.error.code}]: ${outcome.error.message}`);
83
- }
84
- return outcome.data;
85
- }
86
- async play(opts) {
87
- await this.client.play(opts);
88
- }
89
- async stop() {
90
- await this.client.stop();
91
- }
92
- async pause() {
93
- await this.client.pause();
94
- }
95
- async resume() {
96
- await this.client.resume();
97
- }
98
- /** Advance `n` frames (default 1) — `EditorClient.step()` sent `n` times, mirroring `vgai step` run repeatedly. */
99
- async step(n = 1) {
100
- for (let i = 0; i < n; i++) {
101
- await this.client.step();
102
- }
103
- }
104
- /** `'all'` -> `EditorClient.selectAll()` (mirrors `vgai select --all`); otherwise `EditorClient.select(id)` (mirrors `vgai select <entityId>`). */
105
- async select(id) {
106
- if (id === 'all') {
107
- await this.client.selectAll();
108
- return;
109
- }
110
- await this.client.select(id);
111
- }
112
- /** Mirrors `vgai deselect`. */
113
- async deselect() {
114
- await this.client.select(null);
115
- }
116
- /** No `id` -> focus the current selection (mirrors bare `vgai focus`); `id` given -> focus that entity. */
117
- async focus(id) {
118
- if (id !== undefined) {
119
- await this.client.focusEntity(id);
120
- return;
121
- }
122
- await this.client.focusSelection();
123
- }
124
- async view(preset) {
125
- await this.client.viewPreset(preset);
126
- }
127
- /** `vgai show <viewport <scene|game>|inspector|console|build>`'s four sub-verbs, folded into one action name. */
128
- async showPanel(name) {
129
- switch (name) {
130
- case 'viewport-scene':
131
- await this.client.showViewport('scene');
132
- return;
133
- case 'viewport-game':
134
- await this.client.showViewport('game');
135
- return;
136
- case 'inspector':
137
- await this.client.showInspector();
138
- return;
139
- case 'console':
140
- await this.client.toggleConsole();
141
- return;
142
- case 'build':
143
- await this.client.showBuild();
144
- return;
145
- }
146
- }
147
- /** `kind` inferred from `path`'s extension when omitted (`inferAssetKind`) — pass it explicitly to override. */
148
- async openAsset(path, kind) {
149
- await this.client.openAsset(path, kind ?? inferAssetKind(path));
150
- }
151
- /** Captures the editor's native Asset Lab four-view preview for `path` (a project-relative asset path). */
152
- async assetPreview(path, options) {
153
- return this.client.captureAssetPreview({ assetPath: path }, options);
154
- }
155
- async grid(on) {
156
- await this.client.setGrid(on);
157
- }
158
- async helpers(on) {
159
- await this.client.setHelpers(on);
160
- }
161
- async stats(on) {
162
- await this.client.setStats(on);
163
- }
164
- async shading(mode) {
165
- await this.client.setShadingMode(mode);
166
- }
167
- /** Mirrors `vgai status` — the full live editor state as JSON. */
168
- async status() {
169
- return this.client.getState();
170
- }
171
- /** A live viewport PNG (`EditorClient.captureViewport`) — no direct CLI verb exists; this is the closest wire read. */
172
- async screenshot(size) {
173
- return this.client.captureViewport(size);
174
- }
175
- }
package/dist/game.d.ts DELETED
@@ -1,29 +0,0 @@
1
- /**
2
- * `game` — the game-control half of `@vgai/live`'s `{ editor, game, page }`.
3
- * A `GameClient` (`@vgai/probe`) bound to a `RelayTransport` on the resolved
4
- * session's port — the SAME session-wire relay (`POST /__editor/command`,
5
- * `bridge-call`/`bridge-screenshot`/`page-script`) every `EditorClient`
6
- * method already uses. Never forked: this module only WIRES `GameClient` up,
7
- * it does not reimplement any of its methods (`state`/`waitFor`/`events`/
8
- * `input.hold`/`input.tap`/`screenshot`/`command`/`page` — whatever
9
- * `GameClient` exposes is exposed here, unchanged).
10
- */
11
- import { GameClient } from '@vgai/probe';
12
- /**
13
- * Builds a `GameClient` over a `RelayTransport({ port })`.
14
- *
15
- * Fence values (`fenceTick`/`fenceSeq`/`fenceSimSeconds`/`fenceWallMs`) are
16
- * placeholders — tick 0, seq 0, sim-seconds 0, "now" — rather than a real
17
- * play-mode-start snapshot. `@vgai/probe`'s own relay fixture
18
- * (`relay-fixture.ts`'s `waitForRelayBridge`) waits for a FRESH play-mode
19
- * boot before constructing its `game` client, because a test always starts
20
- * from a known instant. A `connect()`ed live session has no such instant —
21
- * it may attach to an ALREADY-RUNNING game session — so there is no single
22
- * "test start" to fence from. Practical effect: `game.events.expect(...)`
23
- * sees the WHOLE event history since play mode started (not a per-call
24
- * window); every other method (`state`/`waitFor`/`input`/`command`/
25
- * `screenshot`) is unaffected by the fence. `pageErrors`/`consoleErrors` are
26
- * always empty — relay mode has no separate page handle to listen on (the
27
- * same documented gap `relay-fixture.ts` carries).
28
- */
29
- export declare function createGameClient(port: number): GameClient;
package/dist/game.js DELETED
@@ -1,40 +0,0 @@
1
- /**
2
- * `game` — the game-control half of `@vgai/live`'s `{ editor, game, page }`.
3
- * A `GameClient` (`@vgai/probe`) bound to a `RelayTransport` on the resolved
4
- * session's port — the SAME session-wire relay (`POST /__editor/command`,
5
- * `bridge-call`/`bridge-screenshot`/`page-script`) every `EditorClient`
6
- * method already uses. Never forked: this module only WIRES `GameClient` up,
7
- * it does not reimplement any of its methods (`state`/`waitFor`/`events`/
8
- * `input.hold`/`input.tap`/`screenshot`/`command`/`page` — whatever
9
- * `GameClient` exposes is exposed here, unchanged).
10
- */
11
- import { GameClient, RelayTransport } from '@vgai/probe';
12
- /**
13
- * Builds a `GameClient` over a `RelayTransport({ port })`.
14
- *
15
- * Fence values (`fenceTick`/`fenceSeq`/`fenceSimSeconds`/`fenceWallMs`) are
16
- * placeholders — tick 0, seq 0, sim-seconds 0, "now" — rather than a real
17
- * play-mode-start snapshot. `@vgai/probe`'s own relay fixture
18
- * (`relay-fixture.ts`'s `waitForRelayBridge`) waits for a FRESH play-mode
19
- * boot before constructing its `game` client, because a test always starts
20
- * from a known instant. A `connect()`ed live session has no such instant —
21
- * it may attach to an ALREADY-RUNNING game session — so there is no single
22
- * "test start" to fence from. Practical effect: `game.events.expect(...)`
23
- * sees the WHOLE event history since play mode started (not a per-call
24
- * window); every other method (`state`/`waitFor`/`input`/`command`/
25
- * `screenshot`) is unaffected by the fence. `pageErrors`/`consoleErrors` are
26
- * always empty — relay mode has no separate page handle to listen on (the
27
- * same documented gap `relay-fixture.ts` carries).
28
- */
29
- export function createGameClient(port) {
30
- return new GameClient({
31
- transport: new RelayTransport({ port }),
32
- pageErrors: [],
33
- consoleErrors: [],
34
- fenceTick: 0,
35
- fenceSeq: 0,
36
- fenceSimSeconds: 0,
37
- fenceWallMs: Date.now(),
38
- warmSession: false,
39
- });
40
- }
package/dist/index.d.ts DELETED
@@ -1,73 +0,0 @@
1
- /**
2
- * `@vgai/live` — `{ editor, game, page }` over the session wire
3
- * (docs/SHARED-SESSION-SPEC.md, Wave 2). Editor control consolidates HERE
4
- * from the ~19 `vgai` CLI verbs: a plain node/tsx script gets
5
- * `import { editor, game, page } from '@vgai/live'` instead of shelling out
6
- * to the CLI — semantics are discoverable from THIS module's types, not from
7
- * CLI usage text.
8
- *
9
- * `@vgai/live` never starts or stops a session — the CLI keeps that
10
- * (`create`/`edit`/`sessions`/`close`). `connect()` (and the lazy
11
- * `editor`/`game`/`page` singletons below) only ATTACH to a session already
12
- * started by `vgai edit`; if none is running for the resolved project, every
13
- * one of them rejects with a clear "run `vgai edit` first" error (see
14
- * `session.ts`'s `resolveSession`) rather than launching anything.
15
- *
16
- * Two ways to use this:
17
- *
18
- * // A: explicit connect() — resolve once, reuse the bound session object.
19
- * import { connect } from '@vgai/live';
20
- * const { editor, game, page } = await connect(); // cwd's project
21
- * const { editor, game, page } = await connect('../other-game');
22
- * await editor.play();
23
- * await game.waitFor((s) => s('score') >= 10, { simSeconds: 5 });
24
- *
25
- * // B: lazy top-level singletons — auto-connect (against cwd) on first
26
- * // use, memoized after that. No explicit connect() call needed.
27
- * import { editor, game, page } from '@vgai/live';
28
- * await editor.grid(true);
29
- * await game.input.tap('jump');
30
- *
31
- * `page(step)` = `GameClient.page(step)` (Wave-2's playwright-shim surface,
32
- * PR #166) — write `step` as a literal `async (page) => {...}` and inline
33
- * every value it needs. KNOWN WIRE LIMITATION: under the relay transport
34
- * this binds to (the same one `vgai probe --in-editor` uses), `step` is
35
- * shipped to the editor dev server as `step.toString()` and reconstructed
36
- * there — closures over outer variables do NOT survive that trip. See
37
- * `@vgai/probe`'s `bridge-transport.ts` (`runPageScript`'s doc comment) and
38
- * `relay-transport.ts` for the full honesty-boundary contract this wraps.
39
- */
40
- import type { GameClient } from '@vgai/probe';
41
- import { LiveEditor } from './editor.js';
42
- import { type ResolvedSession, type SessionResolutionDeps } from './session.js';
43
- export type { EditorClient } from '@vgai/editor-sdk';
44
- export type { GameClient } from '@vgai/probe';
45
- export type { ApplyDiffInput, ApplyDiffResult, PanelName } from './editor.js';
46
- export { inferAssetKind, LiveEditor } from './editor.js';
47
- export { createGameClient } from './game.js';
48
- export type { ResolvedSession, SessionListingTransport, SessionResolutionDeps, } from './session.js';
49
- export { findProjectRootFrom, resolveSession } from './session.js';
50
- export type { LazySession } from './singleton.js';
51
- export { createLazySession } from './singleton.js';
52
- /** A `game.page(step)`-shaped call — see this module's doc comment for the closure-capture limitation. */
53
- export type PageStep = GameClient['page'];
54
- export interface LiveSession {
55
- editor: LiveEditor;
56
- game: GameClient;
57
- /** `GameClient.page` bound to `game` — see this module's own doc comment for the wire limitation. */
58
- page: PageStep;
59
- /** The resolved session this is bound to — useful for logging/debugging which port/project a script attached to. */
60
- session: ResolvedSession;
61
- }
62
- /**
63
- * Resolve `projectDir` (default `process.cwd()`) to its live `vgai edit`
64
- * session and bind `{ editor, game, page }` to it. `deps` is an advanced/
65
- * test-only escape hatch (see `session.ts`'s `SessionResolutionDeps`) — real
66
- * callers never need it.
67
- */
68
- export declare function connect(projectDir?: string, deps?: SessionResolutionDeps): Promise<LiveSession>;
69
- /** Test-only reset of the lazy top-level singletons' memo — NOT part of the documented public surface (real callers never need to reconnect mid-process). */
70
- export declare function __resetLiveSingletonForTests(): void;
71
- export declare const editor: LiveEditor;
72
- export declare const game: GameClient;
73
- export declare const page: PageStep;
package/dist/index.js DELETED
@@ -1,77 +0,0 @@
1
- /**
2
- * `@vgai/live` — `{ editor, game, page }` over the session wire
3
- * (docs/SHARED-SESSION-SPEC.md, Wave 2). Editor control consolidates HERE
4
- * from the ~19 `vgai` CLI verbs: a plain node/tsx script gets
5
- * `import { editor, game, page } from '@vgai/live'` instead of shelling out
6
- * to the CLI — semantics are discoverable from THIS module's types, not from
7
- * CLI usage text.
8
- *
9
- * `@vgai/live` never starts or stops a session — the CLI keeps that
10
- * (`create`/`edit`/`sessions`/`close`). `connect()` (and the lazy
11
- * `editor`/`game`/`page` singletons below) only ATTACH to a session already
12
- * started by `vgai edit`; if none is running for the resolved project, every
13
- * one of them rejects with a clear "run `vgai edit` first" error (see
14
- * `session.ts`'s `resolveSession`) rather than launching anything.
15
- *
16
- * Two ways to use this:
17
- *
18
- * // A: explicit connect() — resolve once, reuse the bound session object.
19
- * import { connect } from '@vgai/live';
20
- * const { editor, game, page } = await connect(); // cwd's project
21
- * const { editor, game, page } = await connect('../other-game');
22
- * await editor.play();
23
- * await game.waitFor((s) => s('score') >= 10, { simSeconds: 5 });
24
- *
25
- * // B: lazy top-level singletons — auto-connect (against cwd) on first
26
- * // use, memoized after that. No explicit connect() call needed.
27
- * import { editor, game, page } from '@vgai/live';
28
- * await editor.grid(true);
29
- * await game.input.tap('jump');
30
- *
31
- * `page(step)` = `GameClient.page(step)` (Wave-2's playwright-shim surface,
32
- * PR #166) — write `step` as a literal `async (page) => {...}` and inline
33
- * every value it needs. KNOWN WIRE LIMITATION: under the relay transport
34
- * this binds to (the same one `vgai probe --in-editor` uses), `step` is
35
- * shipped to the editor dev server as `step.toString()` and reconstructed
36
- * there — closures over outer variables do NOT survive that trip. See
37
- * `@vgai/probe`'s `bridge-transport.ts` (`runPageScript`'s doc comment) and
38
- * `relay-transport.ts` for the full honesty-boundary contract this wraps.
39
- */
40
- import { EditorClient } from '@vgai/editor-sdk';
41
- import { LiveEditor } from './editor.js';
42
- import { createGameClient } from './game.js';
43
- import { lazyChainProxy } from './lazy-proxy.js';
44
- import { resolveSession } from './session.js';
45
- import { createLazySession } from './singleton.js';
46
- export { inferAssetKind, LiveEditor } from './editor.js';
47
- export { createGameClient } from './game.js';
48
- export { findProjectRootFrom, resolveSession } from './session.js';
49
- export { createLazySession } from './singleton.js';
50
- /**
51
- * Resolve `projectDir` (default `process.cwd()`) to its live `vgai edit`
52
- * session and bind `{ editor, game, page }` to it. `deps` is an advanced/
53
- * test-only escape hatch (see `session.ts`'s `SessionResolutionDeps`) — real
54
- * callers never need it.
55
- */
56
- export async function connect(projectDir, deps) {
57
- const resolved = await resolveSession(projectDir, deps);
58
- const client = new EditorClient({ url: `http://localhost:${resolved.port}` });
59
- const editor = new LiveEditor(client, resolved.projectRoot);
60
- const game = createGameClient(resolved.port);
61
- const page = (step) => game.page(step);
62
- return { editor, game, page, session: resolved };
63
- }
64
- // ---------------------------------------------------------------------------
65
- // Lazy top-level singletons (usage form B above) — thin proxies over an
66
- // internal memoized connect() against cwd. First access triggers connect();
67
- // every access after that (across editor/game/page, and across however many
68
- // calls) reuses the SAME resolved session.
69
- // ---------------------------------------------------------------------------
70
- const lazySession = createLazySession(() => connect());
71
- /** Test-only reset of the lazy top-level singletons' memo — NOT part of the documented public surface (real callers never need to reconnect mid-process). */
72
- export function __resetLiveSingletonForTests() {
73
- lazySession.reset();
74
- }
75
- export const editor = lazyChainProxy(() => lazySession.ensure().then((s) => s.editor));
76
- export const game = lazyChainProxy(() => lazySession.ensure().then((s) => s.game));
77
- export const page = lazyChainProxy(() => lazySession.ensure().then((s) => s.page));
@@ -1,26 +0,0 @@
1
- /**
2
- * `lazyChainProxy` — turns an async "resolve the real object" function into
3
- * a synchronously-importable stand-in that supports the SAME call shape as
4
- * the real thing, including nested member access (`game.input.hold(...)`,
5
- * `game.events.expect(...)`), by recording the property-access PATH and only
6
- * resolving + walking it down at the point of an actual function CALL.
7
- *
8
- * This is what makes `index.ts`'s top-level `export const editor = ...` /
9
- * `game` / `page` work as plain values a script can `import { editor, game,
10
- * page } from '@vgai/live'` and call immediately — each call transparently
11
- * awaits the memoized `connect()` first.
12
- *
13
- * Limitation (by design, documented on `index.ts`'s exports too): only
14
- * FUNCTION-shaped access resolves through this proxy — `game.input.hold(x)`
15
- * works, but reading a plain data property (e.g. `game.fenceTick`) would
16
- * return another inert proxy, not the real number, since there is no
17
- * function CALL to trigger resolution. Every documented `editor`/`game`/
18
- * `page` member is a method, so this never bites the documented surface.
19
- */
20
- /**
21
- * `resolveRoot` is called (and its result awaited) on every terminal
22
- * function call reached through the returned proxy — callers typically wire
23
- * it to a memoized `connect()` (see `index.ts`), so repeated calls across
24
- * many proxy invocations still resolve only once.
25
- */
26
- export declare function lazyChainProxy<T>(resolveRoot: () => Promise<unknown>, path?: PropertyKey[]): T;
@@ -1,62 +0,0 @@
1
- /**
2
- * `lazyChainProxy` — turns an async "resolve the real object" function into
3
- * a synchronously-importable stand-in that supports the SAME call shape as
4
- * the real thing, including nested member access (`game.input.hold(...)`,
5
- * `game.events.expect(...)`), by recording the property-access PATH and only
6
- * resolving + walking it down at the point of an actual function CALL.
7
- *
8
- * This is what makes `index.ts`'s top-level `export const editor = ...` /
9
- * `game` / `page` work as plain values a script can `import { editor, game,
10
- * page } from '@vgai/live'` and call immediately — each call transparently
11
- * awaits the memoized `connect()` first.
12
- *
13
- * Limitation (by design, documented on `index.ts`'s exports too): only
14
- * FUNCTION-shaped access resolves through this proxy — `game.input.hold(x)`
15
- * works, but reading a plain data property (e.g. `game.fenceTick`) would
16
- * return another inert proxy, not the real number, since there is no
17
- * function CALL to trigger resolution. Every documented `editor`/`game`/
18
- * `page` member is a method, so this never bites the documented surface.
19
- */
20
- function isFunction(value) {
21
- return typeof value === 'function';
22
- }
23
- function walk(root, path) {
24
- if (path.length === 0)
25
- return { thisArg: undefined, fn: root };
26
- let obj = root;
27
- for (let i = 0; i < path.length - 1; i++) {
28
- const key = path[i];
29
- obj = obj[key];
30
- }
31
- const lastKey = path[path.length - 1];
32
- return { thisArg: obj, fn: obj[lastKey] };
33
- }
34
- /**
35
- * `resolveRoot` is called (and its result awaited) on every terminal
36
- * function call reached through the returned proxy — callers typically wire
37
- * it to a memoized `connect()` (see `index.ts`), so repeated calls across
38
- * many proxy invocations still resolve only once.
39
- */
40
- export function lazyChainProxy(resolveRoot, path = []) {
41
- const callableTarget = (() => { });
42
- return new Proxy(callableTarget, {
43
- get(_target, prop) {
44
- // Never look thenable — `await`ing a proxy (accidentally, or via a
45
- // generic helper checking `typeof x.then`) must not trigger resolution
46
- // or hang; there is no promise here, only a call-shaped stand-in.
47
- if (prop === 'then' || prop === 'catch' || prop === 'finally')
48
- return undefined;
49
- return lazyChainProxy(resolveRoot, [...path, prop]);
50
- },
51
- apply(_target, _thisArg, args) {
52
- return resolveRoot().then((root) => {
53
- const { thisArg, fn } = walk(root, path);
54
- if (!isFunction(fn)) {
55
- const label = path.length > 0 ? path.map(String).join('.') : '(the connected value)';
56
- throw new TypeError(`@vgai/live: ${label} is not a function on the connected session.`);
57
- }
58
- return fn.apply(thisArg, args);
59
- });
60
- },
61
- });
62
- }
package/dist/session.d.ts DELETED
@@ -1,72 +0,0 @@
1
- /**
2
- * Session resolution for `connect()` (`index.ts`) — turns a project
3
- * directory into `{ port, projectRoot }` for an ALREADY-RUNNING `vgai edit`
4
- * session. `@vgai/live` never starts or stops a session (see the module doc
5
- * on `index.ts`); this only finds one.
6
- *
7
- * Two pieces, from two different sources:
8
- *
9
- * 1. Live-session DISCOVERY + PROBING (`HttpEditorTransport.listSessions`,
10
- * `withTimeout`, `EDITOR_SESSION_DISCOVERY_TIMEOUT_MS`) — reused
11
- * directly from `@vgai/sdk`'s `editor/transport.ts` (B3,
12
- * `resolveEditorSession`'s own sibling machinery). That module is a
13
- * real, tested, PUBLICLY EXPORTED part of `@vgai/sdk` — reading
14
- * `~/.vgai/editor-sessions.json`, PID-liveness-filtering it, and
15
- * verifying each survivor with a live `GET /__editor/project` probe is
16
- * exactly the hard, easy-to-get-subtly-wrong part worth reusing rather
17
- * than re-deriving.
18
- *
19
- * 2. Project-root discovery (`findProjectRootFrom` below) — LIFTED (not
20
- * imported) from `packages/vgai-cli/src/project-root.ts`. `@vgai/sdk`'s
21
- * `resolveEditorSession` takes an already-known `ctx.projectRoot`; it has
22
- * no "walk up from an arbitrary cwd to find the nearest vgai.game.json"
23
- * step of its own, which `connect(projectDir?)` needs. `@vgai/cli` is
24
- * `"private": true` with no `exports` field to import through (and
25
- * depending on it from here would invert the intended CLI -> SDK ->
26
- * engine direction anyway — see `@vgai/sdk`'s own architecture note),
27
- * so this ~10-line pure function is duplicated verbatim instead. Keep in
28
- * sync if that logic ever changes.
29
- *
30
- * DELIBERATE DIVERGENCE from `resolveEditorSession` itself: that function's
31
- * documented precedence (see its own doc comment) falls back to "the
32
- * lowest-numbered port among ALL live sessions" when `ctx.projectRoot` is
33
- * given but matches none of them — a reasonable default for a single-agent,
34
- * usually-one-session tool context. `@vgai/live` explicitly does NOT reuse
35
- * that fallback: CLAUDE.md's editor-first workflow is emphatic that a
36
- * project must never be silently retargeted to a DIFFERENT project's editor
37
- * session (a real hazard in a multi-project/fleet setup — several unrelated
38
- * `vgai edit` sessions can easily be running at once). `resolveSession`
39
- * below matches the CLI's own stricter `getClient()` behavior instead: only
40
- * a session whose OWN project matches `projectRoot` is ever returned: no
41
- * match means a clear, actionable error, never a guess.
42
- */
43
- import { type EditorTransport } from '@vgai/sdk';
44
- /**
45
- * Lifted verbatim from `packages/vgai-cli/src/project-root.ts` (see this
46
- * module's doc comment above for why it's duplicated rather than imported).
47
- * Nearest ancestor of `dir` (inclusive) containing a `vgai.game.json`, or
48
- * `null` if none.
49
- */
50
- export declare function findProjectRootFrom(dir: string): string | null;
51
- /** The minimal slice of `EditorTransport` `resolveSession` actually needs — narrower than the full interface so a test double only has to implement one method. */
52
- export type SessionListingTransport = Pick<EditorTransport, 'listSessions'>;
53
- export interface ResolvedSession {
54
- /** Editor dev-server port the resolved session is listening on. */
55
- port: number;
56
- /** Absolute project root — the nearest ancestor of the requested directory containing `vgai.game.json`. */
57
- projectRoot: string;
58
- }
59
- export interface SessionResolutionDeps {
60
- /** Session listing + liveness verification — defaults to a real `HttpEditorTransport` (`@vgai/sdk`). Overridable for tests. */
61
- transport?: SessionListingTransport;
62
- /** Project-root discovery — defaults to the real fs walk (`findProjectRootFrom` above). Overridable for tests. */
63
- findProjectRootFrom?: (dir: string) => string | null;
64
- }
65
- /**
66
- * Resolve `projectDir` (default `process.cwd()`) to the port of its already-
67
- * running `vgai edit` session. Throws a descriptive error (never hangs
68
- * indefinitely — bounded by `EDITOR_SESSION_DISCOVERY_TIMEOUT_MS`, and never
69
- * silently attaches to an unrelated project's session — see the module doc
70
- * above) when no vgai.game.json is found, or no live session covers it.
71
- */
72
- export declare function resolveSession(projectDir?: string, deps?: SessionResolutionDeps): Promise<ResolvedSession>;
package/dist/session.js DELETED
@@ -1,97 +0,0 @@
1
- /**
2
- * Session resolution for `connect()` (`index.ts`) — turns a project
3
- * directory into `{ port, projectRoot }` for an ALREADY-RUNNING `vgai edit`
4
- * session. `@vgai/live` never starts or stops a session (see the module doc
5
- * on `index.ts`); this only finds one.
6
- *
7
- * Two pieces, from two different sources:
8
- *
9
- * 1. Live-session DISCOVERY + PROBING (`HttpEditorTransport.listSessions`,
10
- * `withTimeout`, `EDITOR_SESSION_DISCOVERY_TIMEOUT_MS`) — reused
11
- * directly from `@vgai/sdk`'s `editor/transport.ts` (B3,
12
- * `resolveEditorSession`'s own sibling machinery). That module is a
13
- * real, tested, PUBLICLY EXPORTED part of `@vgai/sdk` — reading
14
- * `~/.vgai/editor-sessions.json`, PID-liveness-filtering it, and
15
- * verifying each survivor with a live `GET /__editor/project` probe is
16
- * exactly the hard, easy-to-get-subtly-wrong part worth reusing rather
17
- * than re-deriving.
18
- *
19
- * 2. Project-root discovery (`findProjectRootFrom` below) — LIFTED (not
20
- * imported) from `packages/vgai-cli/src/project-root.ts`. `@vgai/sdk`'s
21
- * `resolveEditorSession` takes an already-known `ctx.projectRoot`; it has
22
- * no "walk up from an arbitrary cwd to find the nearest vgai.game.json"
23
- * step of its own, which `connect(projectDir?)` needs. `@vgai/cli` is
24
- * `"private": true` with no `exports` field to import through (and
25
- * depending on it from here would invert the intended CLI -> SDK ->
26
- * engine direction anyway — see `@vgai/sdk`'s own architecture note),
27
- * so this ~10-line pure function is duplicated verbatim instead. Keep in
28
- * sync if that logic ever changes.
29
- *
30
- * DELIBERATE DIVERGENCE from `resolveEditorSession` itself: that function's
31
- * documented precedence (see its own doc comment) falls back to "the
32
- * lowest-numbered port among ALL live sessions" when `ctx.projectRoot` is
33
- * given but matches none of them — a reasonable default for a single-agent,
34
- * usually-one-session tool context. `@vgai/live` explicitly does NOT reuse
35
- * that fallback: CLAUDE.md's editor-first workflow is emphatic that a
36
- * project must never be silently retargeted to a DIFFERENT project's editor
37
- * session (a real hazard in a multi-project/fleet setup — several unrelated
38
- * `vgai edit` sessions can easily be running at once). `resolveSession`
39
- * below matches the CLI's own stricter `getClient()` behavior instead: only
40
- * a session whose OWN project matches `projectRoot` is ever returned: no
41
- * match means a clear, actionable error, never a guess.
42
- */
43
- import { existsSync } from 'node:fs';
44
- import { dirname, join, resolve } from 'node:path';
45
- import { EDITOR_SESSION_DISCOVERY_TIMEOUT_MS, HttpEditorTransport, withTimeout, } from '@vgai/sdk';
46
- /**
47
- * Lifted verbatim from `packages/vgai-cli/src/project-root.ts` (see this
48
- * module's doc comment above for why it's duplicated rather than imported).
49
- * Nearest ancestor of `dir` (inclusive) containing a `vgai.game.json`, or
50
- * `null` if none.
51
- */
52
- export function findProjectRootFrom(dir) {
53
- let cur = resolve(dir);
54
- for (;;) {
55
- if (existsSync(join(cur, 'vgai.game.json')))
56
- return cur;
57
- const parent = dirname(cur);
58
- if (parent === cur)
59
- return null;
60
- cur = parent;
61
- }
62
- }
63
- /**
64
- * Resolve `projectDir` (default `process.cwd()`) to the port of its already-
65
- * running `vgai edit` session. Throws a descriptive error (never hangs
66
- * indefinitely — bounded by `EDITOR_SESSION_DISCOVERY_TIMEOUT_MS`, and never
67
- * silently attaches to an unrelated project's session — see the module doc
68
- * above) when no vgai.game.json is found, or no live session covers it.
69
- */
70
- export async function resolveSession(projectDir = process.cwd(), deps = {}) {
71
- const findRoot = deps.findProjectRootFrom ?? findProjectRootFrom;
72
- const transport = deps.transport ?? new HttpEditorTransport();
73
- const projectRoot = findRoot(projectDir);
74
- if (projectRoot === null) {
75
- throw new Error(`@vgai/live: no vgai.game.json found in ${projectDir} or any parent directory — is this a vgai project?`);
76
- }
77
- let sessions;
78
- try {
79
- sessions = await withTimeout(transport.listSessions(EDITOR_SESSION_DISCOVERY_TIMEOUT_MS), EDITOR_SESSION_DISCOVERY_TIMEOUT_MS, 'editor session discovery');
80
- }
81
- catch {
82
- sessions = [];
83
- }
84
- const canon = resolve(projectRoot);
85
- const match = sessions.find((s) => s.project !== null && resolve(s.project) === canon);
86
- if (!match) {
87
- const otherCount = sessions.length;
88
- throw new Error(`@vgai/live: no live editor session found for ${projectRoot}. @vgai/live only attaches to ` +
89
- 'an already-running session — it never starts one — so run `vgai edit` in that project ' +
90
- 'first, then retry.' +
91
- (otherCount > 0
92
- ? ` (${otherCount} other live session(s) found, but none open this project — @vgai/live ` +
93
- 'never silently attaches to a different project.)'
94
- : ''));
95
- }
96
- return { port: match.port, projectRoot };
97
- }
@@ -1,18 +0,0 @@
1
- /**
2
- * A tiny memoized-async-factory primitive — the machinery behind
3
- * `index.ts`'s lazy top-level `editor`/`game`/`page` singletons. Pulled into
4
- * its own module (rather than inlined) so it's independently unit-testable
5
- * with a fake factory, with no need to exercise a real `connect()` (session
6
- * discovery, network) just to prove "two accesses, one connect".
7
- *
8
- * Caches the in-flight PROMISE, not just the resolved value — two callers
9
- * racing `ensure()` before the first resolves still share the SAME
10
- * connection attempt, not two independent ones.
11
- */
12
- export interface LazySession<T> {
13
- /** Returns the memoized promise, creating it via `factory()` on first call. */
14
- ensure(): Promise<T>;
15
- /** Clears the memo — the NEXT `ensure()` calls `factory()` again. Exposed for tests (and for a caller that deliberately wants to reconnect); not needed in ordinary use. */
16
- reset(): void;
17
- }
18
- export declare function createLazySession<T>(factory: () => Promise<T>): LazySession<T>;
package/dist/singleton.js DELETED
@@ -1,12 +0,0 @@
1
- export function createLazySession(factory) {
2
- let promise = null;
3
- return {
4
- ensure() {
5
- promise ??= factory();
6
- return promise;
7
- },
8
- reset() {
9
- promise = null;
10
- },
11
- };
12
- }