@vgai/live 0.5.19 → 0.5.21

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/session.js CHANGED
@@ -92,21 +92,61 @@ function readProjectSession(projectRoot) {
92
92
  return null;
93
93
  }
94
94
  }
95
- async function verifyProjectSession(hint, projectRoot) {
95
+ /**
96
+ * WHICH PROJECT a `/__editor/project` body says its server is serving, and the
97
+ * reason it cannot describe it.
98
+ *
99
+ * `serving` is the server's own statement of "I AM serving this project, I
100
+ * just cannot describe it" — its manifest is unparseable or fails strict
101
+ * validation. The route added it because a bare `{ project: null }` there is
102
+ * indistinguishable from "no project open"; reading only `project.path`
103
+ * reproduces that collapse on this side, and the cost is the worst kind of
104
+ * wrong answer: this door refused a session that WAS open on the caller's
105
+ * project with "no live editor session found … (N other live session(s) found,
106
+ * but none open this project)" — sending the operator to start an editor that
107
+ * was already running, with the real defect (their own manifest) never named.
108
+ */
109
+ function servedProject(body) {
110
+ const b = body;
111
+ return {
112
+ path: b.project?.path ?? b.serving?.path ?? null,
113
+ manifestError: b.project ? null : (b.serving?.error ?? null),
114
+ };
115
+ }
116
+ /**
117
+ * What the server on `port` says it serves — `undefined` when it did not
118
+ * answer at all. One probe: the manifest failure arrives with the path, so
119
+ * neither the hint path nor the refusal path pays a second round trip.
120
+ */
121
+ async function probeServedProject(port) {
96
122
  try {
97
- const response = await fetch(`http://127.0.0.1:${hint.port}/__editor/project`, {
123
+ const response = await fetch(`http://127.0.0.1:${port}/__editor/project`, {
98
124
  signal: AbortSignal.timeout(EDITOR_SESSION_DISCOVERY_TIMEOUT_MS),
99
125
  });
100
126
  if (!response.ok)
101
- return false;
102
- const body = (await response.json());
103
- const servedProject = body.project?.path;
104
- return (servedProject !== undefined && canonicalPath(servedProject) === canonicalPath(projectRoot));
127
+ return undefined;
128
+ return servedProject(await response.json());
105
129
  }
106
130
  catch {
107
- return false;
131
+ return undefined;
108
132
  }
109
133
  }
134
+ /**
135
+ * The refusal for a session that IS this project's and cannot be driven,
136
+ * because the project's manifest does not load.
137
+ *
138
+ * A refusal, not an attach: with no readable manifest the editor page itself
139
+ * renders its startup-error screen, so there is no editor and no game behind
140
+ * this door to bind `{ editor, game, page, tools, session }` to. What changed
141
+ * is only that it now says the true thing — the failing file and key — instead
142
+ * of "no live editor session found", which sent operators to start an editor
143
+ * that was already running while their actual defect went unnamed.
144
+ */
145
+ function manifestRefusal(projectRoot, manifestError) {
146
+ return new Error(`@vgai/live: the editor session for ${projectRoot} is live, but its vgai.project.json does ` +
147
+ 'not load, so there is no editor or game to drive — the editor page is showing this same ' +
148
+ `error. Fix the manifest and retry; the session recovers on save, no restart needed.\n${manifestError}`);
149
+ }
110
150
  /**
111
151
  * Resolve `projectDir` (default `process.cwd()`) to the port of its already-
112
152
  * running `vgai edit` session. Throws a descriptive error (never hangs
@@ -127,9 +167,24 @@ export async function resolveSession(projectDir = process.cwd(), deps = {}) {
127
167
  // deadline. The live probe remains authoritative, so a stale file cannot
128
168
  // attach us to the wrong project or port.
129
169
  const localHint = (deps.readProjectSession ?? readProjectSession)(projectRoot);
130
- if (localHint &&
131
- (await (deps.verifyProjectSession ?? verifyProjectSession)(localHint, projectRoot))) {
132
- return { port: localHint.port, projectRoot };
170
+ if (localHint) {
171
+ // `deps.verifyProjectSession` is the test-only override and keeps its
172
+ // boolean meaning: "this hint is this project's session, and healthy".
173
+ // With no override we read the probe ourselves, because the same response
174
+ // carries WHY a served project cannot be described.
175
+ if (deps.verifyProjectSession) {
176
+ if (await deps.verifyProjectSession(localHint, projectRoot)) {
177
+ return { port: localHint.port, projectRoot };
178
+ }
179
+ }
180
+ else {
181
+ const served = await probeServedProject(localHint.port);
182
+ if (served?.path != null && canonicalPath(served.path) === canonicalPath(projectRoot)) {
183
+ if (served.manifestError !== null)
184
+ throw manifestRefusal(projectRoot, served.manifestError);
185
+ return { port: localHint.port, projectRoot };
186
+ }
187
+ }
133
188
  }
134
189
  let sessions;
135
190
  try {
@@ -150,5 +205,7 @@ export async function resolveSession(projectDir = process.cwd(), deps = {}) {
150
205
  'never silently attaches to a different project.)'
151
206
  : ''));
152
207
  }
208
+ if (match.manifestError != null)
209
+ throw manifestRefusal(projectRoot, match.manifestError);
153
210
  return { port: match.port, projectRoot };
154
211
  }
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.5.19",
5
+ "version": "0.5.21",
6
6
  "type": "module",
7
7
  "repository": {
8
8
  "type": "git",
@@ -32,8 +32,8 @@
32
32
  "prepack": "npm run build"
33
33
  },
34
34
  "dependencies": {
35
- "@vgai/editor-sdk": "0.5.19",
36
- "@vgai/sdk": "0.5.19"
35
+ "@vgai/editor-sdk": "0.5.21",
36
+ "@vgai/sdk": "0.5.21"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@playwright/test": ">=1.58.2 <2"
package/src/session.ts CHANGED
@@ -130,25 +130,69 @@ function readProjectSession(projectRoot: string): ProjectSessionHint | null {
130
130
  }
131
131
  }
132
132
 
133
- async function verifyProjectSession(
134
- hint: ProjectSessionHint,
135
- projectRoot: string,
136
- ): Promise<boolean> {
133
+ /**
134
+ * WHICH PROJECT a `/__editor/project` body says its server is serving, and the
135
+ * reason it cannot describe it.
136
+ *
137
+ * `serving` is the server's own statement of "I AM serving this project, I
138
+ * just cannot describe it" — its manifest is unparseable or fails strict
139
+ * validation. The route added it because a bare `{ project: null }` there is
140
+ * indistinguishable from "no project open"; reading only `project.path`
141
+ * reproduces that collapse on this side, and the cost is the worst kind of
142
+ * wrong answer: this door refused a session that WAS open on the caller's
143
+ * project with "no live editor session found … (N other live session(s) found,
144
+ * but none open this project)" — sending the operator to start an editor that
145
+ * was already running, with the real defect (their own manifest) never named.
146
+ */
147
+ function servedProject(body: unknown): { path: string | null; manifestError: string | null } {
148
+ const b = body as {
149
+ project?: { path?: string } | null;
150
+ serving?: { path?: string; error?: string } | null;
151
+ };
152
+ return {
153
+ path: b.project?.path ?? b.serving?.path ?? null,
154
+ manifestError: b.project ? null : (b.serving?.error ?? null),
155
+ };
156
+ }
157
+
158
+ /**
159
+ * What the server on `port` says it serves — `undefined` when it did not
160
+ * answer at all. One probe: the manifest failure arrives with the path, so
161
+ * neither the hint path nor the refusal path pays a second round trip.
162
+ */
163
+ async function probeServedProject(
164
+ port: number,
165
+ ): Promise<{ path: string | null; manifestError: string | null } | undefined> {
137
166
  try {
138
- const response = await fetch(`http://127.0.0.1:${hint.port}/__editor/project`, {
167
+ const response = await fetch(`http://127.0.0.1:${port}/__editor/project`, {
139
168
  signal: AbortSignal.timeout(EDITOR_SESSION_DISCOVERY_TIMEOUT_MS),
140
169
  });
141
- if (!response.ok) return false;
142
- const body = (await response.json()) as { project?: { path?: string } | null };
143
- const servedProject = body.project?.path;
144
- return (
145
- servedProject !== undefined && canonicalPath(servedProject) === canonicalPath(projectRoot)
146
- );
170
+ if (!response.ok) return undefined;
171
+ return servedProject(await response.json());
147
172
  } catch {
148
- return false;
173
+ return undefined;
149
174
  }
150
175
  }
151
176
 
177
+ /**
178
+ * The refusal for a session that IS this project's and cannot be driven,
179
+ * because the project's manifest does not load.
180
+ *
181
+ * A refusal, not an attach: with no readable manifest the editor page itself
182
+ * renders its startup-error screen, so there is no editor and no game behind
183
+ * this door to bind `{ editor, game, page, tools, session }` to. What changed
184
+ * is only that it now says the true thing — the failing file and key — instead
185
+ * of "no live editor session found", which sent operators to start an editor
186
+ * that was already running while their actual defect went unnamed.
187
+ */
188
+ function manifestRefusal(projectRoot: string, manifestError: string): Error {
189
+ return new Error(
190
+ `@vgai/live: the editor session for ${projectRoot} is live, but its vgai.project.json does ` +
191
+ 'not load, so there is no editor or game to drive — the editor page is showing this same ' +
192
+ `error. Fix the manifest and retry; the session recovers on save, no restart needed.\n${manifestError}`,
193
+ );
194
+ }
195
+
152
196
  /**
153
197
  * Resolve `projectDir` (default `process.cwd()`) to the port of its already-
154
198
  * running `vgai edit` session. Throws a descriptive error (never hangs
@@ -176,11 +220,22 @@ export async function resolveSession(
176
220
  // deadline. The live probe remains authoritative, so a stale file cannot
177
221
  // attach us to the wrong project or port.
178
222
  const localHint = (deps.readProjectSession ?? readProjectSession)(projectRoot);
179
- if (
180
- localHint &&
181
- (await (deps.verifyProjectSession ?? verifyProjectSession)(localHint, projectRoot))
182
- ) {
183
- return { port: localHint.port, projectRoot };
223
+ if (localHint) {
224
+ // `deps.verifyProjectSession` is the test-only override and keeps its
225
+ // boolean meaning: "this hint is this project's session, and healthy".
226
+ // With no override we read the probe ourselves, because the same response
227
+ // carries WHY a served project cannot be described.
228
+ if (deps.verifyProjectSession) {
229
+ if (await deps.verifyProjectSession(localHint, projectRoot)) {
230
+ return { port: localHint.port, projectRoot };
231
+ }
232
+ } else {
233
+ const served = await probeServedProject(localHint.port);
234
+ if (served?.path != null && canonicalPath(served.path) === canonicalPath(projectRoot)) {
235
+ if (served.manifestError !== null) throw manifestRefusal(projectRoot, served.manifestError);
236
+ return { port: localHint.port, projectRoot };
237
+ }
238
+ }
184
239
  }
185
240
 
186
241
  let sessions: EditorSessionInfo[];
@@ -209,5 +264,7 @@ export async function resolveSession(
209
264
  );
210
265
  }
211
266
 
267
+ if (match.manifestError != null) throw manifestRefusal(projectRoot, match.manifestError);
268
+
212
269
  return { port: match.port, projectRoot };
213
270
  }