@vgai/live 0.5.2 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -0
- package/dist/editor.d.ts +78 -55
- package/dist/editor.js +129 -76
- package/dist/game-client/bridge-heartbeat.d.ts +49 -0
- package/dist/game-client/bridge-heartbeat.js +46 -0
- package/dist/game-client/bridge-transport.d.ts +75 -0
- package/dist/game-client/bridge-transport.js +19 -0
- package/dist/game-client/client.d.ts +293 -0
- package/dist/game-client/client.js +706 -0
- package/dist/game-client/errors.d.ts +57 -0
- package/dist/game-client/errors.js +76 -0
- package/dist/game-client/events-matcher.d.ts +41 -0
- package/dist/game-client/events-matcher.js +68 -0
- package/dist/game-client/failure-block.d.ts +93 -0
- package/dist/game-client/failure-block.js +97 -0
- package/dist/game-client/fast-forward.d.ts +125 -0
- package/dist/game-client/fast-forward.js +122 -0
- package/dist/game-client/hidden-recovery.d.ts +85 -0
- package/dist/game-client/hidden-recovery.js +105 -0
- package/dist/game-client/index.d.ts +40 -0
- package/dist/game-client/index.js +26 -0
- package/dist/game-client/perf-sampling.d.ts +56 -0
- package/dist/game-client/perf-sampling.js +85 -0
- package/dist/game-client/relay-transport.d.ts +100 -0
- package/dist/game-client/relay-transport.js +237 -0
- package/dist/game-client/screenshot-target.d.ts +60 -0
- package/dist/game-client/screenshot-target.js +68 -0
- package/dist/game-client/state-cap.d.ts +7 -0
- package/dist/game-client/state-cap.js +21 -0
- package/dist/game-client/types.d.ts +128 -0
- package/dist/game-client/types.js +15 -0
- package/dist/game-client/wait-for.d.ts +155 -0
- package/dist/game-client/wait-for.js +229 -0
- package/dist/game.d.ts +47 -18
- package/dist/game.js +59 -16
- package/dist/index.d.ts +46 -21
- package/dist/index.js +51 -20
- package/dist/session.d.ts +4 -4
- package/dist/session.js +7 -7
- package/dist/tools.d.ts +12 -3
- package/dist/tools.js +15 -6
- package/package.json +10 -5
- package/src/editor.ts +142 -96
- package/src/game-client/bridge-heartbeat.ts +61 -0
- package/src/game-client/bridge-transport.ts +73 -0
- package/src/game-client/client.ts +836 -0
- package/src/game-client/errors.ts +96 -0
- package/src/game-client/events-matcher.ts +106 -0
- package/src/game-client/failure-block.ts +199 -0
- package/src/game-client/fast-forward.ts +175 -0
- package/src/game-client/hidden-recovery.ts +149 -0
- package/src/game-client/index.ts +98 -0
- package/src/game-client/perf-sampling.ts +94 -0
- package/src/game-client/relay-transport.ts +311 -0
- package/src/game-client/screenshot-target.ts +91 -0
- package/src/game-client/state-cap.ts +29 -0
- package/src/game-client/types.ts +137 -0
- package/src/game-client/wait-for.ts +327 -0
- package/src/game.ts +96 -16
- package/src/index.ts +68 -31
- package/src/session.ts +8 -10
- package/src/tools.ts +19 -6
package/src/game.ts
CHANGED
|
@@ -1,36 +1,74 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `game` — the game-control half of `@vgai/live`'s `{ editor, game, page }`.
|
|
3
|
-
* A `GameClient` (
|
|
3
|
+
* A `GameClient` (`./game-client/`) bound to a `RelayTransport` on the resolved
|
|
4
4
|
* session's port — the SAME session-wire relay (`POST /__editor/command`,
|
|
5
5
|
* `bridge-call`/`bridge-screenshot`/`page-script`) every `EditorClient`
|
|
6
6
|
* method already uses. Never forked: this module only WIRES `GameClient` up,
|
|
7
7
|
* it does not reimplement any of its methods (`state`/`waitFor`/`events`/
|
|
8
8
|
* `input.hold`/`input.tap`/`screenshot`/`command`/`page` — whatever
|
|
9
9
|
* `GameClient` exposes is exposed here, unchanged).
|
|
10
|
+
*
|
|
11
|
+
* When the editor has MORE THAN ONE instance mounted (multiplayer authoring —
|
|
12
|
+
* see the engine's play-mode `mountAdditionalInstance`), `game` alone REFUSES
|
|
13
|
+
* to guess which one a call addresses. `game.instances()` / `game.instance(id)`
|
|
14
|
+
* are the way past that: each returns a `GameClient` whose relay carries a
|
|
15
|
+
* specific mount id (`RelayTransport({ instance })`), so one script can drive
|
|
16
|
+
* several instances — two seats of a match, or one scene under two seeds —
|
|
17
|
+
* without ambiguity.
|
|
10
18
|
*/
|
|
11
19
|
|
|
12
|
-
import { GameClient, RelayTransport } from '
|
|
20
|
+
import { GameClient, RelayTransport } from './game-client/index.js';
|
|
21
|
+
|
|
22
|
+
/** A `GameClient` addressing ONE mounted instance, tagged with the mount `id`
|
|
23
|
+
* it drives. `id` is the whole point of the handle to a CALLER: it is what
|
|
24
|
+
* `tools.run(name, args, { instance: handle.id })` passes to scope a
|
|
25
|
+
* node-hosted tool (e.g. `project.autoplay`) to this seat — the two-seat
|
|
26
|
+
* multiplayer verification pattern. Without it the caller has a client it can
|
|
27
|
+
* drive but no id to hand a tool, so the seat-scoped path silently degrades to
|
|
28
|
+
* the sole-instance one. */
|
|
29
|
+
export type AddressedGameClient = GameClient & { readonly id: string };
|
|
30
|
+
|
|
31
|
+
/** `game` plus the instance-addressing surface (see this module's doc). The
|
|
32
|
+
* base `GameClient` methods are unchanged; a bare call still targets the sole
|
|
33
|
+
* instance and refuses when several are live. */
|
|
34
|
+
export interface LiveGame extends GameClient {
|
|
35
|
+
/**
|
|
36
|
+
* A handle addressing ONE mounted instance by its mount id — a `GameClient`
|
|
37
|
+
* whose every relay call carries `instance: id`, and which exposes that `id`
|
|
38
|
+
* back (`handle.id === id`). Synchronous: it builds a client, it does not
|
|
39
|
+
* contact the editor. (Through the bare top-level `game` singleton it
|
|
40
|
+
* resolves a session first and so returns a promise — the same proxy
|
|
41
|
+
* limitation the singleton documents; the `connect()`/`vgai eval` form is
|
|
42
|
+
* synchronous.)
|
|
43
|
+
*/
|
|
44
|
+
instance(id: string): AddressedGameClient;
|
|
45
|
+
/**
|
|
46
|
+
* Handles for every instance currently mounted, in mount order — each tagged
|
|
47
|
+
* with its own `id`. One round trip (`list-instances`) to enumerate, then one
|
|
48
|
+
* addressed `GameClient` per id. `[]` when nothing is mounted (not playing) —
|
|
49
|
+
* the honest answer.
|
|
50
|
+
*/
|
|
51
|
+
instances(): Promise<AddressedGameClient[]>;
|
|
52
|
+
}
|
|
13
53
|
|
|
14
54
|
/**
|
|
15
|
-
*
|
|
55
|
+
* Build a `GameClient` on `port`, optionally addressing one mounted instance.
|
|
16
56
|
*
|
|
17
57
|
* Fence values (`fenceTick`/`fenceSeq`/`fenceSimSeconds`/`fenceWallMs`) are
|
|
18
58
|
* placeholders — tick 0, seq 0, sim-seconds 0, "now" — rather than a real
|
|
19
|
-
* play-mode-start snapshot.
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* always empty — relay mode has no separate page handle to listen on (the
|
|
29
|
-
* same documented gap `relay-fixture.ts` carries).
|
|
59
|
+
* play-mode-start snapshot. A caller that owns its own browser can fence off
|
|
60
|
+
* a FRESH play-mode boot, because it starts from a known instant. A
|
|
61
|
+
* `connect()`ed live session has no such instant — it may attach to an
|
|
62
|
+
* ALREADY-RUNNING game session — so there is no single "run start" to fence
|
|
63
|
+
* from. Practical effect: `game.events.expect(...)` sees the WHOLE event
|
|
64
|
+
* history since play mode started (not a per-call window); every other method
|
|
65
|
+
* (`state`/`waitFor`/`input`/`command`/`screenshot`) is unaffected by the
|
|
66
|
+
* fence. `pageErrors`/`consoleErrors` are always empty — relay mode has no
|
|
67
|
+
* separate page handle to listen on.
|
|
30
68
|
*/
|
|
31
|
-
|
|
69
|
+
function gameClientFor(port: number, artifactsDir?: string, instance?: string): GameClient {
|
|
32
70
|
return new GameClient({
|
|
33
|
-
transport: new RelayTransport({ port }),
|
|
71
|
+
transport: new RelayTransport(instance === undefined ? { port } : { port, instance }),
|
|
34
72
|
pageErrors: [],
|
|
35
73
|
consoleErrors: [],
|
|
36
74
|
fenceTick: 0,
|
|
@@ -41,3 +79,45 @@ export function createGameClient(port: number, artifactsDir?: string): GameClien
|
|
|
41
79
|
artifactsDir,
|
|
42
80
|
});
|
|
43
81
|
}
|
|
82
|
+
|
|
83
|
+
/** The unaddressed `game` client — targets the sole live instance and refuses
|
|
84
|
+
* when several are mounted. Kept as a named export for callers/tests that
|
|
85
|
+
* want just the base client. */
|
|
86
|
+
export function createGameClient(port: number, artifactsDir?: string): GameClient {
|
|
87
|
+
return gameClientFor(port, artifactsDir);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Query the editor's live instance ids over the session wire.
|
|
91
|
+
*
|
|
92
|
+
* `list-instances` is a session-level command (`command-listener.ts`), NOT a
|
|
93
|
+
* `bridge-call` method: it answers "which instances exist", and the
|
|
94
|
+
* bridge-call path RESOLVES an instance (refusing when several are live), so
|
|
95
|
+
* routing this through it would hit the exact ambiguity it throws on. The
|
|
96
|
+
* response spreads `data` at the top level, so the ids arrive as
|
|
97
|
+
* `body.instances`. */
|
|
98
|
+
async function listInstanceIds(port: number): Promise<string[]> {
|
|
99
|
+
const res = await fetch(`http://127.0.0.1:${port}/__editor/command`, {
|
|
100
|
+
method: 'POST',
|
|
101
|
+
headers: { 'Content-Type': 'application/json' },
|
|
102
|
+
body: JSON.stringify({ type: 'list-instances' }),
|
|
103
|
+
});
|
|
104
|
+
const body = (await res.json()) as { ok?: boolean; instances?: unknown; error?: string };
|
|
105
|
+
if (!body.ok || !Array.isArray(body.instances)) {
|
|
106
|
+
throw new Error(`@vgai/live: list-instances failed — ${body.error ?? 'no instances in reply'}`);
|
|
107
|
+
}
|
|
108
|
+
return body.instances.map(String);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Build the `LiveGame` — the base `game` client plus its instance-addressing
|
|
112
|
+
* surface. */
|
|
113
|
+
export function createLiveGame(port: number, artifactsDir?: string): LiveGame {
|
|
114
|
+
const base = gameClientFor(port, artifactsDir);
|
|
115
|
+
// Tag each addressed handle with the mount id it drives, so a caller can pass
|
|
116
|
+
// `handle.id` to `tools.run(..., { instance })`. The id is already known here
|
|
117
|
+
// (it is what parameterizes the relay); attaching it just hands it back.
|
|
118
|
+
const instance = (id: string): AddressedGameClient =>
|
|
119
|
+
Object.assign(gameClientFor(port, artifactsDir, id), { id });
|
|
120
|
+
const instances = async (): Promise<AddressedGameClient[]> =>
|
|
121
|
+
(await listInstanceIds(port)).map(instance);
|
|
122
|
+
return Object.assign(base, { instance, instances });
|
|
123
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@vgai/live` — `{ editor, game, page, tools }` over the session wire
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* CLI usage text.
|
|
2
|
+
* `@vgai/live` — `{ editor, game, page, tools }` over the session wire.
|
|
3
|
+
* Editor control consolidates HERE from the ~19 `vgai` CLI verbs: a plain
|
|
4
|
+
* node/tsx script gets `import { editor, game, page } from '@vgai/live'`
|
|
5
|
+
* instead of shelling out to the CLI — semantics are discoverable from THIS
|
|
6
|
+
* module's types, not from CLI usage text.
|
|
8
7
|
*
|
|
9
8
|
* `@vgai/live` never starts or stops a session — the CLI keeps that
|
|
10
9
|
* (`create`/`edit`/`sessions`/`close`). `connect()` (and the lazy
|
|
@@ -30,25 +29,24 @@
|
|
|
30
29
|
*
|
|
31
30
|
* `page(step)` = `GameClient.page(step)` (Wave-2's playwright-shim surface,
|
|
32
31
|
* PR #166) — write `step` as a literal `async (page) => {...}` and inline
|
|
33
|
-
* every value it needs. KNOWN WIRE LIMITATION: under the relay transport
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
32
|
+
* every value it needs. KNOWN WIRE LIMITATION: under the relay transport this
|
|
33
|
+
* binds to, `step` is shipped to the editor dev server as `step.toString()`
|
|
34
|
+
* and reconstructed there — closures over outer variables do NOT survive that
|
|
35
|
+
* trip. See `game-client/bridge-transport.ts` (`runPageScript`'s doc comment)
|
|
36
|
+
* and `game-client/relay-transport.ts` for the full honesty-boundary contract
|
|
37
|
+
* this wraps.
|
|
39
38
|
*/
|
|
40
39
|
|
|
41
40
|
import { join } from 'node:path';
|
|
42
|
-
import type { GameClient } from '@vgai/e2e';
|
|
43
41
|
import { EditorClient } from '@vgai/editor-sdk';
|
|
44
42
|
import { LiveEditor } from './editor.js';
|
|
45
|
-
import {
|
|
43
|
+
import { createLiveGame, type LiveGame } from './game.js';
|
|
44
|
+
import type { GameClient } from './game-client/index.js';
|
|
46
45
|
import { lazyChainProxy } from './lazy-proxy.js';
|
|
47
46
|
import { type ResolvedSession, resolveSession, type SessionResolutionDeps } from './session.js';
|
|
48
47
|
import { createLazySession } from './singleton.js';
|
|
49
48
|
import { LiveTools } from './tools.js';
|
|
50
49
|
|
|
51
|
-
export type { GameClient } from '@vgai/e2e';
|
|
52
50
|
export type {
|
|
53
51
|
ActiveDocumentCapture,
|
|
54
52
|
EditorClient,
|
|
@@ -56,9 +54,10 @@ export type {
|
|
|
56
54
|
EditorViewDocument,
|
|
57
55
|
PresentedEditorView,
|
|
58
56
|
} from '@vgai/editor-sdk';
|
|
59
|
-
export type {
|
|
57
|
+
export type { PanelName } from './editor.js';
|
|
60
58
|
export { inferAssetKind, LiveEditor } from './editor.js';
|
|
61
|
-
export { createGameClient } from './game.js';
|
|
59
|
+
export { createGameClient, createLiveGame, type LiveGame } from './game.js';
|
|
60
|
+
export * from './game-client/index.js';
|
|
62
61
|
export type {
|
|
63
62
|
ProjectSessionHint,
|
|
64
63
|
ResolvedSession,
|
|
@@ -73,17 +72,63 @@ export { LiveTools } from './tools.js';
|
|
|
73
72
|
/** A `game.page(step)`-shaped call — see this module's doc comment for the closure-capture limitation. */
|
|
74
73
|
export type PageStep = GameClient['page'];
|
|
75
74
|
|
|
76
|
-
export interface LiveSession {
|
|
75
|
+
export interface LiveSession extends LiveBindings {
|
|
76
|
+
/** The resolved session this is bound to — useful for logging/debugging which port/project a script attached to. */
|
|
77
|
+
session: ResolvedSession;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The callable half of a session: everything `vgai eval` binds except `session` itself. */
|
|
81
|
+
export interface LiveBindings {
|
|
77
82
|
editor: LiveEditor;
|
|
78
|
-
game
|
|
83
|
+
/** The game client, plus `instances()`/`instance(id)` for addressing one of
|
|
84
|
+
* several mounted instances (multiplayer authoring). A bare `game` call
|
|
85
|
+
* targets the sole instance and refuses when several are live. */
|
|
86
|
+
game: LiveGame;
|
|
79
87
|
/** `GameClient.page` bound to `game` — see this module's own doc comment for the wire limitation. */
|
|
80
88
|
page: PageStep;
|
|
81
89
|
/** Registered project callables: enumerate, inspect, and invoke. */
|
|
82
90
|
tools: LiveTools;
|
|
83
|
-
/** The resolved session this is bound to — useful for logging/debugging which port/project a script attached to. */
|
|
84
|
-
session: ResolvedSession;
|
|
85
91
|
}
|
|
86
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Construct the binding graph for an editor on `port`. Pure — every object
|
|
95
|
+
* here is built from a URL/port and contacts nothing until a method is
|
|
96
|
+
* awaited, which is what lets `unconnectedBindings()` below reuse it.
|
|
97
|
+
*/
|
|
98
|
+
function bindTo(port: number, projectRoot: string): LiveBindings {
|
|
99
|
+
const client = new EditorClient({ url: `http://127.0.0.1:${port}` });
|
|
100
|
+
const editor = new LiveEditor(client);
|
|
101
|
+
const game = createLiveGame(port, join(projectRoot, '.vgai', 'last-run'));
|
|
102
|
+
const page: PageStep = (step) => game.page(step);
|
|
103
|
+
const tools = new LiveTools(client);
|
|
104
|
+
return { editor, game, page, tools };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The SAME `{ editor, game, page, tools }` objects `connect()` returns —
|
|
109
|
+
* real classes, real instance fields — bound to a port nothing listens on.
|
|
110
|
+
*
|
|
111
|
+
* This exists for INTROSPECTION, and specifically for `vgai eval --list`
|
|
112
|
+
* (`packages/vgai-cli/src/eval-surface.ts`), which must answer "what could I
|
|
113
|
+
* call?" before `vgai edit` is running. Reading a CLASS instead is what
|
|
114
|
+
* shipped first, and it silently omitted `game.input` and `game.events`:
|
|
115
|
+
* those are instance fields the constructor assigns, so they exist on no
|
|
116
|
+
* prototype and a class walk cannot see them — an agent reading the listing
|
|
117
|
+
* concluded there was no honest input door. Objects, not classes, is the fix,
|
|
118
|
+
* and going through `bindTo` is what keeps it honest: the probe is assembled
|
|
119
|
+
* by the same function `connect()` uses, so a binding added there is listed
|
|
120
|
+
* here with no second place to remember.
|
|
121
|
+
*
|
|
122
|
+
* Awaiting any method on these objects will fail (connection refused). Use
|
|
123
|
+
* `connect()` for that.
|
|
124
|
+
*/
|
|
125
|
+
export function unconnectedBindings(): LiveBindings {
|
|
126
|
+
return bindTo(UNCONNECTED_PORT, process.cwd());
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Port 0 is never a listening editor — an accidental call fails loudly rather than reaching a stranger's server. */
|
|
130
|
+
const UNCONNECTED_PORT = 0;
|
|
131
|
+
|
|
87
132
|
/**
|
|
88
133
|
* Resolve `projectDir` (default `process.cwd()`) to its live `vgai edit`
|
|
89
134
|
* session and bind `{ editor, game, page }` to it. `deps` is an advanced/
|
|
@@ -95,15 +140,7 @@ export async function connect(
|
|
|
95
140
|
deps?: SessionResolutionDeps,
|
|
96
141
|
): Promise<LiveSession> {
|
|
97
142
|
const resolved = await resolveSession(projectDir, deps);
|
|
98
|
-
|
|
99
|
-
const editor = new LiveEditor(client, resolved.projectRoot);
|
|
100
|
-
const game = createGameClient(
|
|
101
|
-
resolved.port,
|
|
102
|
-
join(resolved.projectRoot, '.vgai', 'e2e', 'last-run'),
|
|
103
|
-
);
|
|
104
|
-
const page: PageStep = (step) => game.page(step);
|
|
105
|
-
const tools = new LiveTools(client);
|
|
106
|
-
return { editor, game, page, tools, session: resolved };
|
|
143
|
+
return { ...bindTo(resolved.port, resolved.projectRoot), session: resolved };
|
|
107
144
|
}
|
|
108
145
|
|
|
109
146
|
// ---------------------------------------------------------------------------
|
|
@@ -123,7 +160,7 @@ export function __resetLiveSingletonForTests(): void {
|
|
|
123
160
|
export const editor: LiveEditor = lazyChainProxy<LiveEditor>(() =>
|
|
124
161
|
lazySession.ensure().then((s) => s.editor),
|
|
125
162
|
);
|
|
126
|
-
export const game:
|
|
163
|
+
export const game: LiveGame = lazyChainProxy<LiveGame>(() =>
|
|
127
164
|
lazySession.ensure().then((s) => s.game),
|
|
128
165
|
);
|
|
129
166
|
export const page: PageStep = lazyChainProxy<PageStep>(() =>
|
package/src/session.ts
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* 3. Project-root discovery (`findProjectRootFrom` below) — LIFTED (not
|
|
24
24
|
* imported) from `packages/vgai-cli/src/project-root.ts`. `@vgai/sdk`'s
|
|
25
25
|
* `resolveEditorSession` takes an already-known `ctx.projectRoot`; it has
|
|
26
|
-
* no "walk up from an arbitrary cwd to find the nearest vgai.
|
|
26
|
+
* no "walk up from an arbitrary cwd to find the nearest vgai.project.json"
|
|
27
27
|
* step of its own, which `connect(projectDir?)` needs. `@vgai/cli` is
|
|
28
28
|
* `"private": true` with no `exports` field to import through (and
|
|
29
29
|
* depending on it from here would invert the intended CLI -> SDK ->
|
|
@@ -58,13 +58,13 @@ import {
|
|
|
58
58
|
/**
|
|
59
59
|
* Lifted verbatim from `packages/vgai-cli/src/project-root.ts` (see this
|
|
60
60
|
* module's doc comment above for why it's duplicated rather than imported).
|
|
61
|
-
* Nearest ancestor of `dir` (inclusive) containing a `vgai.
|
|
61
|
+
* Nearest ancestor of `dir` (inclusive) containing a `vgai.project.json`, or
|
|
62
62
|
* `null` if none.
|
|
63
63
|
*/
|
|
64
64
|
export function findProjectRootFrom(dir: string): string | null {
|
|
65
65
|
let cur = resolve(dir);
|
|
66
66
|
for (;;) {
|
|
67
|
-
if (existsSync(join(cur, 'vgai.
|
|
67
|
+
if (existsSync(join(cur, 'vgai.project.json'))) return cur;
|
|
68
68
|
const parent = dirname(cur);
|
|
69
69
|
if (parent === cur) return null;
|
|
70
70
|
cur = parent;
|
|
@@ -77,7 +77,7 @@ export type SessionListingTransport = Pick<EditorTransport, 'listSessions'>;
|
|
|
77
77
|
export interface ResolvedSession {
|
|
78
78
|
/** Editor dev-server port the resolved session is listening on. */
|
|
79
79
|
port: number;
|
|
80
|
-
/** Absolute project root — the nearest ancestor of the requested directory containing `vgai.
|
|
80
|
+
/** Absolute project root — the nearest ancestor of the requested directory containing `vgai.project.json`. */
|
|
81
81
|
projectRoot: string;
|
|
82
82
|
}
|
|
83
83
|
|
|
@@ -135,7 +135,7 @@ async function verifyProjectSession(
|
|
|
135
135
|
projectRoot: string,
|
|
136
136
|
): Promise<boolean> {
|
|
137
137
|
try {
|
|
138
|
-
const response = await fetch(`http://
|
|
138
|
+
const response = await fetch(`http://127.0.0.1:${hint.port}/__editor/project`, {
|
|
139
139
|
signal: AbortSignal.timeout(EDITOR_SESSION_DISCOVERY_TIMEOUT_MS),
|
|
140
140
|
});
|
|
141
141
|
if (!response.ok) return false;
|
|
@@ -154,7 +154,7 @@ async function verifyProjectSession(
|
|
|
154
154
|
* running `vgai edit` session. Throws a descriptive error (never hangs
|
|
155
155
|
* indefinitely — bounded by `EDITOR_SESSION_DISCOVERY_TIMEOUT_MS`, and never
|
|
156
156
|
* silently attaches to an unrelated project's session — see the module doc
|
|
157
|
-
* above) when no vgai.
|
|
157
|
+
* above) when no vgai.project.json is found, or no live session covers it.
|
|
158
158
|
*/
|
|
159
159
|
export async function resolveSession(
|
|
160
160
|
projectDir: string = process.cwd(),
|
|
@@ -166,7 +166,7 @@ export async function resolveSession(
|
|
|
166
166
|
const projectRoot = findRoot(projectDir);
|
|
167
167
|
if (projectRoot === null) {
|
|
168
168
|
throw new Error(
|
|
169
|
-
`@vgai/live: no vgai.
|
|
169
|
+
`@vgai/live: no vgai.project.json found in ${projectDir} or any parent directory — is this a vgai project?`,
|
|
170
170
|
);
|
|
171
171
|
}
|
|
172
172
|
|
|
@@ -195,9 +195,7 @@ export async function resolveSession(
|
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
const canon = canonicalPath(projectRoot);
|
|
198
|
-
const match = sessions.find(
|
|
199
|
-
(s) => s.kind !== 'e2e' && s.project !== null && canonicalPath(s.project) === canon,
|
|
200
|
-
);
|
|
198
|
+
const match = sessions.find((s) => s.project !== null && canonicalPath(s.project) === canon);
|
|
201
199
|
if (!match) {
|
|
202
200
|
const otherCount = sessions.length;
|
|
203
201
|
throw new Error(
|
package/src/tools.ts
CHANGED
|
@@ -8,11 +8,16 @@ import type {
|
|
|
8
8
|
} from '@vgai/editor-sdk';
|
|
9
9
|
|
|
10
10
|
export class LiveTools {
|
|
11
|
-
|
|
11
|
+
/** `#`-private for the same reason `LiveEditor.#client` is. */
|
|
12
|
+
readonly #client: EditorClient;
|
|
13
|
+
|
|
14
|
+
constructor(client: EditorClient) {
|
|
15
|
+
this.#client = client;
|
|
16
|
+
}
|
|
12
17
|
|
|
13
18
|
/** Enumerate the exact `package.json#vgai.tools` catalog without executing it. */
|
|
14
19
|
async list(): Promise<ProjectToolCatalog> {
|
|
15
|
-
return this
|
|
20
|
+
return this.#client.listProjectTools();
|
|
16
21
|
}
|
|
17
22
|
|
|
18
23
|
/** Return one tool's discoverable metadata, or `null` when it is not registered. */
|
|
@@ -21,12 +26,20 @@ export class LiveTools {
|
|
|
21
26
|
return catalog.tools.find((tool) => tool.name === name) ?? null;
|
|
22
27
|
}
|
|
23
28
|
|
|
24
|
-
/**
|
|
25
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Invoke the same validated callable used by the editor and CLI.
|
|
31
|
+
*
|
|
32
|
+
* `instance` names WHICH mounted instance the tool should drive when several
|
|
33
|
+
* are live (multiplayer authoring) — it reaches the tool as `ctx.instance`,
|
|
34
|
+
* and a tool that drives the game (e.g. `project.autoplay`) binds
|
|
35
|
+
* `game.instance(ctx.instance)` from it. Omitted is the single-instance case;
|
|
36
|
+
* the tool then targets the sole live instance, exactly as before.
|
|
37
|
+
*/
|
|
38
|
+
async run(
|
|
26
39
|
name: string,
|
|
27
40
|
input: unknown = {},
|
|
28
|
-
options: { confirm?: boolean } = {},
|
|
41
|
+
options: { confirm?: boolean; instance?: string } = {},
|
|
29
42
|
): Promise<ProjectToolOutcome> {
|
|
30
|
-
return this
|
|
43
|
+
return this.#client.runProjectTool(name, input, options);
|
|
31
44
|
}
|
|
32
45
|
}
|