@vgai/live 0.4.0-canary.20260715.0
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/editor.d.ts +100 -0
- package/dist/editor.js +175 -0
- package/dist/game.d.ts +29 -0
- package/dist/game.js +40 -0
- package/dist/index.d.ts +73 -0
- package/dist/index.js +77 -0
- package/dist/lazy-proxy.d.ts +26 -0
- package/dist/lazy-proxy.js +62 -0
- package/dist/session.d.ts +72 -0
- package/dist/session.js +97 -0
- package/dist/singleton.d.ts +18 -0
- package/dist/singleton.js +12 -0
- package/package.json +45 -0
- package/src/editor.ts +229 -0
- package/src/game.ts +42 -0
- package/src/index.ts +115 -0
- package/src/lazy-proxy.ts +68 -0
- package/src/session.ts +135 -0
- package/src/singleton.ts +30 -0
package/src/session.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
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
|
+
|
|
44
|
+
import { existsSync } from 'node:fs';
|
|
45
|
+
import { dirname, join, resolve } from 'node:path';
|
|
46
|
+
import {
|
|
47
|
+
EDITOR_SESSION_DISCOVERY_TIMEOUT_MS,
|
|
48
|
+
type EditorSessionInfo,
|
|
49
|
+
type EditorTransport,
|
|
50
|
+
HttpEditorTransport,
|
|
51
|
+
withTimeout,
|
|
52
|
+
} from '@vgai/sdk';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Lifted verbatim from `packages/vgai-cli/src/project-root.ts` (see this
|
|
56
|
+
* module's doc comment above for why it's duplicated rather than imported).
|
|
57
|
+
* Nearest ancestor of `dir` (inclusive) containing a `vgai.game.json`, or
|
|
58
|
+
* `null` if none.
|
|
59
|
+
*/
|
|
60
|
+
export function findProjectRootFrom(dir: string): string | null {
|
|
61
|
+
let cur = resolve(dir);
|
|
62
|
+
for (;;) {
|
|
63
|
+
if (existsSync(join(cur, 'vgai.game.json'))) return cur;
|
|
64
|
+
const parent = dirname(cur);
|
|
65
|
+
if (parent === cur) return null;
|
|
66
|
+
cur = parent;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The minimal slice of `EditorTransport` `resolveSession` actually needs — narrower than the full interface so a test double only has to implement one method. */
|
|
71
|
+
export type SessionListingTransport = Pick<EditorTransport, 'listSessions'>;
|
|
72
|
+
|
|
73
|
+
export interface ResolvedSession {
|
|
74
|
+
/** Editor dev-server port the resolved session is listening on. */
|
|
75
|
+
port: number;
|
|
76
|
+
/** Absolute project root — the nearest ancestor of the requested directory containing `vgai.game.json`. */
|
|
77
|
+
projectRoot: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface SessionResolutionDeps {
|
|
81
|
+
/** Session listing + liveness verification — defaults to a real `HttpEditorTransport` (`@vgai/sdk`). Overridable for tests. */
|
|
82
|
+
transport?: SessionListingTransport;
|
|
83
|
+
/** Project-root discovery — defaults to the real fs walk (`findProjectRootFrom` above). Overridable for tests. */
|
|
84
|
+
findProjectRootFrom?: (dir: string) => string | null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Resolve `projectDir` (default `process.cwd()`) to the port of its already-
|
|
89
|
+
* running `vgai edit` session. Throws a descriptive error (never hangs
|
|
90
|
+
* indefinitely — bounded by `EDITOR_SESSION_DISCOVERY_TIMEOUT_MS`, and never
|
|
91
|
+
* silently attaches to an unrelated project's session — see the module doc
|
|
92
|
+
* above) when no vgai.game.json is found, or no live session covers it.
|
|
93
|
+
*/
|
|
94
|
+
export async function resolveSession(
|
|
95
|
+
projectDir: string = process.cwd(),
|
|
96
|
+
deps: SessionResolutionDeps = {},
|
|
97
|
+
): Promise<ResolvedSession> {
|
|
98
|
+
const findRoot = deps.findProjectRootFrom ?? findProjectRootFrom;
|
|
99
|
+
const transport = deps.transport ?? new HttpEditorTransport();
|
|
100
|
+
|
|
101
|
+
const projectRoot = findRoot(projectDir);
|
|
102
|
+
if (projectRoot === null) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`@vgai/live: no vgai.game.json found in ${projectDir} or any parent directory — is this a vgai project?`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let sessions: EditorSessionInfo[];
|
|
109
|
+
try {
|
|
110
|
+
sessions = await withTimeout(
|
|
111
|
+
transport.listSessions(EDITOR_SESSION_DISCOVERY_TIMEOUT_MS),
|
|
112
|
+
EDITOR_SESSION_DISCOVERY_TIMEOUT_MS,
|
|
113
|
+
'editor session discovery',
|
|
114
|
+
);
|
|
115
|
+
} catch {
|
|
116
|
+
sessions = [];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const canon = resolve(projectRoot);
|
|
120
|
+
const match = sessions.find((s) => s.project !== null && resolve(s.project) === canon);
|
|
121
|
+
if (!match) {
|
|
122
|
+
const otherCount = sessions.length;
|
|
123
|
+
throw new Error(
|
|
124
|
+
`@vgai/live: no live editor session found for ${projectRoot}. @vgai/live only attaches to ` +
|
|
125
|
+
'an already-running session — it never starts one — so run `vgai edit` in that project ' +
|
|
126
|
+
'first, then retry.' +
|
|
127
|
+
(otherCount > 0
|
|
128
|
+
? ` (${otherCount} other live session(s) found, but none open this project — @vgai/live ` +
|
|
129
|
+
'never silently attaches to a different project.)'
|
|
130
|
+
: ''),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { port: match.port, projectRoot };
|
|
135
|
+
}
|
package/src/singleton.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
|
|
19
|
+
export function createLazySession<T>(factory: () => Promise<T>): LazySession<T> {
|
|
20
|
+
let promise: Promise<T> | null = null;
|
|
21
|
+
return {
|
|
22
|
+
ensure(): Promise<T> {
|
|
23
|
+
promise ??= factory();
|
|
24
|
+
return promise;
|
|
25
|
+
},
|
|
26
|
+
reset(): void {
|
|
27
|
+
promise = null;
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|