@volter/live 0.5.210 → 0.5.212
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/BUNDLED_NOTICES +13 -13
- package/README.md +7 -11
- package/dist/chunk-7SZ3C4CC.js +768 -0
- package/dist/chunk-7SZ3C4CC.js.map +7 -0
- package/dist/chunk-7WKVJGHZ.js +15 -0
- package/dist/chunk-7WKVJGHZ.js.map +7 -0
- package/dist/chunk-SDIWRX5X.js +189 -0
- package/dist/chunk-SDIWRX5X.js.map +7 -0
- package/dist/chunk-USXOIBVX.js +81 -0
- package/dist/chunk-USXOIBVX.js.map +7 -0
- package/dist/chunk-Y5W77HYT.js +123 -0
- package/dist/chunk-Y5W77HYT.js.map +7 -0
- package/dist/chunk-YPAOTPIF.js +202 -0
- package/dist/chunk-YPAOTPIF.js.map +7 -0
- package/dist/client.d.ts +370 -0
- package/dist/client.js +9 -0
- package/dist/client.js.map +7 -0
- package/dist/discovery.d.ts +32 -0
- package/dist/discovery.js +19 -0
- package/dist/discovery.js.map +7 -0
- package/dist/editor-document.d.ts +12 -47
- package/dist/editor.d.ts +34 -181
- package/dist/http-transport.d.ts +6 -0
- package/dist/index.d.ts +1 -5
- package/dist/index.js +76 -1722
- package/dist/index.js.map +4 -4
- package/dist/installed-package.d.ts +6 -0
- package/dist/installed-package.js +7 -0
- package/dist/installed-package.js.map +7 -0
- package/dist/lazy-proxy.d.ts +11 -11
- package/dist/member-hints.d.ts +6 -9
- package/dist/product-create.d.ts +29 -0
- package/dist/product-create.js +13 -0
- package/dist/product-create.js.map +7 -0
- package/dist/product-locator.d.ts +100 -0
- package/dist/product-locator.js +25 -0
- package/dist/product-locator.js.map +7 -0
- package/dist/registry-format.d.ts +44 -0
- package/dist/registry-format.js +21 -0
- package/dist/registry-format.js.map +7 -0
- package/dist/session.d.ts +10 -3
- package/dist/singleton.d.ts +1 -13
- package/dist/volter-manifest.d.ts +55 -0
- package/dist/volter-manifest.js +9 -0
- package/dist/volter-manifest.js.map +7 -0
- package/dist/workbench-locator.d.ts +84 -0
- package/dist/workbench-locator.js +416 -0
- package/dist/workbench-locator.js.map +7 -0
- package/package.json +35 -3
- package/src/client.ts +1052 -0
- package/src/discovery.ts +105 -0
- package/src/editor-document.ts +12 -49
- package/src/editor.ts +45 -212
- package/src/http-transport.ts +19 -0
- package/src/index.ts +3 -7
- package/src/installed-package.ts +15 -0
- package/src/lazy-proxy.ts +11 -11
- package/src/member-hints.ts +8 -12
- package/src/product-create.ts +38 -0
- package/src/product-locator.ts +327 -0
- package/src/registry-format.ts +185 -0
- package/src/session.ts +38 -66
- package/src/singleton.ts +1 -16
- package/src/volter-manifest.ts +97 -0
- package/src/workbench-locator.ts +625 -0
- package/dist/tools.d.ts +0 -23
- package/src/tools.ts +0 -45
package/dist/index.js
CHANGED
|
@@ -1,1158 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
code;
|
|
18
|
-
/**
|
|
19
|
-
* True when the RELAY ended the command itself rather than the editor
|
|
20
|
-
* answering it — the HTTP 504 that `server/server-utils.ts`'s
|
|
21
|
-
* `commandResponseFor` gives any `timedOut` result, or this client's own
|
|
22
|
-
* deadline below.
|
|
23
|
-
*
|
|
24
|
-
* Read it as "no answer", not as "the budget expired". `editor-server.ts`
|
|
25
|
-
* raises `timedOut` for five conditions and only one of them takes the full
|
|
26
|
-
* budget: the command's timer expiring, the controlling tab's socket dying,
|
|
27
|
-
* the receipt window closing unanswered, a beating-but-dead tab, and no tab
|
|
28
|
-
* present at all. The last four can fail in milliseconds.
|
|
29
|
-
*
|
|
30
|
-
* A caller that converges by retrying (the editor's `restart` command) needs the distinction
|
|
31
|
-
* because a refusal the editor ANSWERED may go differently next time, while
|
|
32
|
-
* a command the relay abandoned tells you nothing new on a second identical
|
|
33
|
-
* attempt — and when the abandonment was a 120s budget, re-running it three
|
|
34
|
-
* times is `restart-readiness.ts`'s 361-seconds-of-silence defect.
|
|
35
|
-
*/
|
|
36
|
-
timedOut;
|
|
37
|
-
constructor(message, code, timedOut = false) {
|
|
38
|
-
super(message);
|
|
39
|
-
this.name = "EditorCommandError";
|
|
40
|
-
this.code = code;
|
|
41
|
-
this.timedOut = timedOut;
|
|
42
|
-
}
|
|
43
|
-
};
|
|
44
|
-
var COMMAND_DEADLINE_MS = 15e4;
|
|
45
|
-
var BLENDER_DEADLINE_MS = 30 * 6e4;
|
|
46
|
-
var UNDICI_DEFAULT_HEADERS_TIMEOUT_MS = 3e5;
|
|
47
|
-
var CONSOLE_DRAIN_TIMEOUT_MS = 1500;
|
|
48
|
-
function describeFetchFailure(error) {
|
|
49
|
-
const messages = [];
|
|
50
|
-
let current = error;
|
|
51
|
-
for (let depth = 0; depth < 8; depth++) {
|
|
52
|
-
if (!(current instanceof Error)) break;
|
|
53
|
-
if (current.message) messages.push(current.message);
|
|
54
|
-
const code = current.code;
|
|
55
|
-
if (typeof code === "string" && code !== "") {
|
|
56
|
-
return { code, detail: messages.join(" <- ") };
|
|
57
|
-
}
|
|
58
|
-
const aggregate = current.errors;
|
|
59
|
-
if (Array.isArray(aggregate) && aggregate.length > 0) {
|
|
60
|
-
const inner = describeFetchFailure(aggregate[0]);
|
|
61
|
-
return { code: inner.code, detail: [...messages, inner.detail].join(" <- ") };
|
|
62
|
-
}
|
|
63
|
-
current = current.cause;
|
|
64
|
-
}
|
|
65
|
-
return { code: "UNKNOWN", detail: messages.join(" <- ") || String(error) };
|
|
66
|
-
}
|
|
67
|
-
var RETRYABLE_TRANSPORT_CODES = /* @__PURE__ */ new Set([
|
|
68
|
-
"ECONNREFUSED",
|
|
69
|
-
"ECONNRESET",
|
|
70
|
-
"EPIPE",
|
|
71
|
-
"ETIMEDOUT",
|
|
72
|
-
"EHOSTUNREACH",
|
|
73
|
-
"UND_ERR_SOCKET",
|
|
74
|
-
"UND_ERR_CONNECT_TIMEOUT"
|
|
75
|
-
]);
|
|
76
|
-
var TRANSPORT_RETRY_DELAY_MS = 400;
|
|
77
|
-
var EditorClient = class {
|
|
78
|
-
baseUrl;
|
|
79
|
-
fetchOverride;
|
|
80
|
-
/**
|
|
81
|
-
* The running game's debug plane — see {@link GameDebugDoor}. It rides the
|
|
82
|
-
* SAME `/__editor/command` relay every other method here uses (relay cases
|
|
83
|
-
* `inspect-gameplay-state` / `invoke-debug-command`, `command-listener.ts`),
|
|
84
|
-
* so a tool contribution reaches the game through the client it already has
|
|
85
|
-
* rather than a second channel of its own.
|
|
86
|
-
*/
|
|
87
|
-
game;
|
|
88
|
-
/**
|
|
89
|
-
* Called with the raw body of EVERY response this client receives — command
|
|
90
|
-
* envelopes and `/__editor/state` alike, on success AND on refusal.
|
|
91
|
-
*
|
|
92
|
-
* It exists for exactly one contract: the server stamps `unresolvedConsole`
|
|
93
|
-
* onto every envelope (`server-utils.ts`'s `commandResponseFor`), and the CLI
|
|
94
|
-
* has to see those counts to be loud about them. Routing that through a
|
|
95
|
-
* single observer here — rather than teaching each of the CLI's output sites
|
|
96
|
-
* to unpack a response — is what keeps the loudness contract ONE mechanism.
|
|
97
|
-
* The observer must not throw; anything it raises is swallowed, because a
|
|
98
|
-
* reporting hook may never break the command it is reporting on.
|
|
99
|
-
*/
|
|
100
|
-
transport;
|
|
101
|
-
onEnvelope;
|
|
102
|
-
constructor(opts) {
|
|
103
|
-
if (opts !== void 0 && (typeof opts !== "object" || opts === null || Array.isArray(opts))) {
|
|
104
|
-
throw new TypeError(
|
|
105
|
-
'EditorClient options must be an object. Use new EditorClient({ url: "http://127.0.0.1:20173" }), not new EditorClient("...").'
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
const unknownOptions = Object.keys(opts ?? {}).filter(
|
|
109
|
-
(key) => key !== "url" && key !== "onEnvelope" && key !== "transport" && key !== "fetch"
|
|
110
|
-
);
|
|
111
|
-
if (unknownOptions.length > 0) {
|
|
112
|
-
throw new Error(
|
|
113
|
-
`EditorClient: unknown option${unknownOptions.length === 1 ? "" : "s"} ${unknownOptions.map((key) => `"${key}"`).join(", ")}. Use { url: "http://127.0.0.1:<port>" } to target an editor.`
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
this.baseUrl = (opts?.url ?? DEFAULT_URL).replace(/\/$/, "");
|
|
117
|
-
this.fetchOverride = opts?.fetch;
|
|
118
|
-
this.onEnvelope = opts?.onEnvelope ?? null;
|
|
119
|
-
this.transport = opts?.transport ?? null;
|
|
120
|
-
this.game = {
|
|
121
|
-
state: async (name) => {
|
|
122
|
-
const data = await this.command({
|
|
123
|
-
type: "inspect-gameplay-state",
|
|
124
|
-
keys: [name]
|
|
125
|
-
});
|
|
126
|
-
return data.state?.[name];
|
|
127
|
-
},
|
|
128
|
-
command: async (name, ...args) => (await this.command({ type: "invoke-debug-command", name, args })).result
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* `retryTransport` opts a command into ONE automatic retry after a transport
|
|
133
|
-
* failure (see {@link RETRYABLE_TRANSPORT_CODES}). It is deliberately
|
|
134
|
-
* OPT-IN and off by default: a socket that died after the request was written
|
|
135
|
-
* cannot prove the editor did not already run the command, so a blanket retry
|
|
136
|
-
* would risk playing/stopping/writing twice. Read-only relays — the captures —
|
|
137
|
-
* have no such hazard and turn it on.
|
|
138
|
-
*/
|
|
139
|
-
async command(body, options) {
|
|
140
|
-
const type = String(body["type"] ?? "command");
|
|
141
|
-
const deadlineMs = options?.deadlineMs ?? COMMAND_DEADLINE_MS;
|
|
142
|
-
if (this.transport) {
|
|
143
|
-
const answered = await this.transport(body);
|
|
144
|
-
if (!answered.ok) {
|
|
145
|
-
throw new EditorCommandError(
|
|
146
|
-
answered.error ?? `Editor command "${type}" failed`,
|
|
147
|
-
answered.code
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
return answered;
|
|
151
|
-
}
|
|
152
|
-
const url = `${this.baseUrl}/__editor/command`;
|
|
153
|
-
const request = JSON.stringify(body);
|
|
154
|
-
let res;
|
|
155
|
-
let retried = false;
|
|
156
|
-
let commandDispatcher;
|
|
157
|
-
for (; ; ) {
|
|
158
|
-
try {
|
|
159
|
-
const init = {
|
|
160
|
-
method: "POST",
|
|
161
|
-
headers: { "Content-Type": "application/json" },
|
|
162
|
-
body: request,
|
|
163
|
-
signal: AbortSignal.timeout(deadlineMs)
|
|
164
|
-
};
|
|
165
|
-
commandDispatcher = deadlineMs > UNDICI_DEFAULT_HEADERS_TIMEOUT_MS ? createDispatcher(deadlineMs + 3e4) : void 0;
|
|
166
|
-
res = this.fetchOverride ? await this.fetchOverride(url, init) : await dispatchFetch(url, init, commandDispatcher);
|
|
167
|
-
break;
|
|
168
|
-
} catch (error) {
|
|
169
|
-
await commandDispatcher?.destroy?.();
|
|
170
|
-
commandDispatcher = void 0;
|
|
171
|
-
if (error?.name === "TimeoutError") {
|
|
172
|
-
throw new EditorCommandError(
|
|
173
|
-
`The editor at ${this.baseUrl} never answered "${type}" within ${Math.round(deadlineMs / 1e3)}s \u2014 past every server-side budget, so the server itself is not answering. Check the terminal that started this editor session.`,
|
|
174
|
-
void 0,
|
|
175
|
-
true
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
const { code, detail } = describeFetchFailure(error);
|
|
179
|
-
if (options?.retryTransport === true && !retried && RETRYABLE_TRANSPORT_CODES.has(code)) {
|
|
180
|
-
retried = true;
|
|
181
|
-
await new Promise((resolve3) => setTimeout(resolve3, TRANSPORT_RETRY_DELAY_MS));
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
throw new EditorCommandError(
|
|
185
|
-
`POST ${url} ("${type}") never reached the editor: ${code}${detail ? ` (${detail})` : ""}.` + (retried ? ` Retried once after ${TRANSPORT_RETRY_DELAY_MS}ms; it failed the same way.` : "") + (options?.retryTransport === true ? "" : " Not retried automatically: this command can change editor state, and a socket that died after the request was written cannot prove the editor did not already run it.") + " A transport failure means the port stopped answering, not that the editor refused \u2014 the dev server restarts on any watched source edit, and it shuts itself down after an idle window. Check the terminal that started this editor session and confirm the port this client resolved.",
|
|
186
|
-
code,
|
|
187
|
-
false
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
let data;
|
|
192
|
-
try {
|
|
193
|
-
data = await this.readJson(res);
|
|
194
|
-
} finally {
|
|
195
|
-
await commandDispatcher?.destroy?.();
|
|
196
|
-
}
|
|
197
|
-
if (!data.ok) {
|
|
198
|
-
throw new EditorCommandError(
|
|
199
|
-
data.error ?? `Editor command failed: ${res.status}`,
|
|
200
|
-
data.code,
|
|
201
|
-
// 504 is every `timedOut` result (`commandResponseFor`) — the relay
|
|
202
|
-
// gave up, on any of its five grounds. A 200 body with `ok: false` is
|
|
203
|
-
// an ANSWER from the editor, however unwelcome. See `timedOut` above.
|
|
204
|
-
res.status === 504
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
return data;
|
|
208
|
-
}
|
|
209
|
-
// --- Play control ---
|
|
210
|
-
/** `opts.seed` (D15/T-D15.6, objection-4 fix) — the editor's `play --seed <n>` command's
|
|
211
|
-
* explicit config leg, relayed as `cmd['seed']`; `handleCommand`'s
|
|
212
|
-
* `'play'` case threads it into `enterPlayMode`'s highest-precedence seed
|
|
213
|
-
* argument (beats manifest.determinism.defaultSeed/?volter-seed=). Omitted,
|
|
214
|
-
* boot seeding falls back to that precedence unchanged.
|
|
215
|
-
*
|
|
216
|
-
* `opts.name` (the editor's `play --name <text>` command) — an OPTIONAL label for this run,
|
|
217
|
-
* relayed as `cmd['name']` and slugified server-side into the run's
|
|
218
|
-
* `logs/play-*.jsonl` filename and its session-journal line. Findability
|
|
219
|
-
* only: no registry, no uniqueness, no lookup verb — grep and `ls` are the
|
|
220
|
-
* query engine. Omitted, the filename keeps its exact unnamed shape. */
|
|
221
|
-
/* `opts.record` (the editor's `play --record <name>` command) — NAMES this run's recording
|
|
222
|
-
* file. It does not ENABLE recording: every relayed play records, with no
|
|
223
|
-
* flag (see `@volter/editor-game`'s `src/play/play-recording.ts`). Omitted, the clip is named for
|
|
224
|
-
* the durable Gameplay Session; named, it becomes an explicit keepsake in
|
|
225
|
-
* `.volter/recordings/<name>.webm`. */
|
|
226
|
-
async play(opts) {
|
|
227
|
-
return this.command({
|
|
228
|
-
type: "play",
|
|
229
|
-
...opts?.seed !== void 0 ? { seed: opts.seed } : {},
|
|
230
|
-
...opts?.name ? { name: opts.name } : {},
|
|
231
|
-
...opts?.record ? { record: opts.record } : {}
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
/** Dispose the current play session and mount it again from fresh project entry source. */
|
|
235
|
-
async restart() {
|
|
236
|
-
await this.command({ type: "play" });
|
|
237
|
-
}
|
|
238
|
-
/** Refuse shutdown if a document cannot be saved. Does not stop the server. */
|
|
239
|
-
async prepareClose() {
|
|
240
|
-
await this.command({ type: "session-prepare-close" });
|
|
241
|
-
}
|
|
242
|
-
/** Stops play, and finalizes this run's recording before the surface it was
|
|
243
|
-
* photographing is torn down. The capture is absent when nothing recorded. */
|
|
244
|
-
async stop() {
|
|
245
|
-
return this.command({ type: "stop" });
|
|
246
|
-
}
|
|
247
|
-
async pause() {
|
|
248
|
-
await this.command({ type: "pause" });
|
|
249
|
-
}
|
|
250
|
-
async resume() {
|
|
251
|
-
await this.command({ type: "resume" });
|
|
252
|
-
}
|
|
253
|
-
async step() {
|
|
254
|
-
await this.command({ type: "step" });
|
|
255
|
-
}
|
|
256
|
-
// --- Selection ---
|
|
257
|
-
async select(id) {
|
|
258
|
-
await this.command({ type: "select", id });
|
|
259
|
-
}
|
|
260
|
-
async selectMultiple(ids) {
|
|
261
|
-
await this.command({ type: "select-multiple", ids });
|
|
262
|
-
}
|
|
263
|
-
async selectAll() {
|
|
264
|
-
await this.command({ type: "select-all" });
|
|
265
|
-
}
|
|
266
|
-
// --- Viewport ---
|
|
267
|
-
async focusEntity(id) {
|
|
268
|
-
await this.command({ type: "focus-entity", id });
|
|
269
|
-
}
|
|
270
|
-
async focusSelection() {
|
|
271
|
-
await this.command({ type: "focus-selection" });
|
|
272
|
-
}
|
|
273
|
-
/**
|
|
274
|
-
* Frame the EDIT viewport camera on one entity — the strict sibling of
|
|
275
|
-
* {@link focusEntity}. Same framing; an id the scene does not know is a
|
|
276
|
-
* refusal naming the id (`EditorCommandError`, code `ENTITY_NOT_FOUND`)
|
|
277
|
-
* rather than `focusEntity`'s silent no-op, so a caller that frames an
|
|
278
|
-
* entity before capturing it cannot photograph the wrong thing.
|
|
279
|
-
*/
|
|
280
|
-
async frameEntity(id) {
|
|
281
|
-
await this.command({ type: "frame-entity", id });
|
|
282
|
-
}
|
|
283
|
-
async viewPreset(preset) {
|
|
284
|
-
await this.command({ type: "view-preset", preset });
|
|
285
|
-
}
|
|
286
|
-
/**
|
|
287
|
-
* LOOK AROUND THE OPEN MODEL, visibly. Swings the active Object3D
|
|
288
|
-
* document's camera — the one on the human's screen — by `azimuth`/
|
|
289
|
-
* `elevation` radians, animated over `duration` seconds, and resolves when
|
|
290
|
-
* the move ends. A human drag during the move cancels it where it stands
|
|
291
|
-
* (`cancelledBy: 'human'`); the promise still resolves.
|
|
292
|
-
*/
|
|
293
|
-
async orbitDocument(options) {
|
|
294
|
-
return this.command({ type: "document-orbit", ...options });
|
|
295
|
-
}
|
|
296
|
-
/** What a frame of a 3D document's stage costs, uncapped (`document-frame-cost`). */
|
|
297
|
-
async frameCostDocument(options) {
|
|
298
|
-
return this.command({ type: "document-frame-cost", ...options });
|
|
299
|
-
}
|
|
300
|
-
/** A slow full revolution around the open document's subject, at a constant rate. */
|
|
301
|
-
async turntableDocument(options) {
|
|
302
|
-
return this.command({ type: "document-turntable", ...options });
|
|
303
|
-
}
|
|
304
|
-
/**
|
|
305
|
-
* Frame the open document's subject (its selection if it has one). `fit`
|
|
306
|
-
* scales the fitted distance: 1 is the toolbar Frame button's tight fit.
|
|
307
|
-
*/
|
|
308
|
-
async frameDocument(fit) {
|
|
309
|
-
return this.command({
|
|
310
|
-
type: "document-frame",
|
|
311
|
-
...fit === void 0 ? {} : { fit }
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
async setCamera(position, target, fov) {
|
|
315
|
-
await this.command({
|
|
316
|
-
type: "set-camera",
|
|
317
|
-
position,
|
|
318
|
-
target,
|
|
319
|
-
...fov === void 0 ? {} : { fov }
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
async captureViewport(size) {
|
|
323
|
-
const data = await this.command({
|
|
324
|
-
type: "capture-viewport",
|
|
325
|
-
...size === void 0 ? {} : { size }
|
|
326
|
-
});
|
|
327
|
-
return { base64: data.base64, mimeType: data.mimeType };
|
|
328
|
-
}
|
|
329
|
-
/**
|
|
330
|
-
* Unit 4 (live-front-door wave) — capture the RUNNING GAME (the editor's
|
|
331
|
-
* `screenshot`'s wire leg). Sends the SAME `bridge-screenshot` relay op
|
|
332
|
-
* `@volter/game-live`'s `RelayTransport.screenshot` (and therefore
|
|
333
|
-
* `game.screenshot()` on the relay path) already sends, so all three
|
|
334
|
-
* surfaces composite the identical full game stack — canvas(es) plus the
|
|
335
|
-
* HUD/react DOM layers — rather than any of them inventing a second,
|
|
336
|
-
* subtly-different capture path. Contrast {@link captureViewport}, which
|
|
337
|
-
* captures the EDITOR viewport's canvas and would silently hand back an
|
|
338
|
-
* editor-only (HUD-less, possibly not-even-playing) image.
|
|
339
|
-
*
|
|
340
|
-
* Rejects — loudly, via `command`'s own `{ok:false}` unwrap — when play
|
|
341
|
-
* mode isn't running ("not in play mode — start play before using the
|
|
342
|
-
* debug seam") or no game canvas is mounted yet. Never returns a blank or
|
|
343
|
-
* editor-only frame as a stand-in.
|
|
344
|
-
*
|
|
345
|
-
* `opts.refreshStarvedFrame` is the loop-starvation leg: without recent rAF
|
|
346
|
-
* progress the canvas holds a provably stale frame and the relay
|
|
347
|
-
* refuses it with `BRIDGE_SCREENSHOT_STALE` rather than pass it off as
|
|
348
|
-
* current. Setting this asks the relay to render exactly ONE deterministic
|
|
349
|
-
* tick (`runTicks(1, {render:'last'})`) first — the same escape
|
|
350
|
-
* `@volter/game-live`'s `RelayTransport.screenshot` has always used, which is why
|
|
351
|
-
* the editor's `eval` command could recover these frames while the editor's `screenshot` command could not.
|
|
352
|
-
* Off by default: a caller who does not ask must never be handed a frame
|
|
353
|
-
* that only exists because the capture drove the game.
|
|
354
|
-
*/
|
|
355
|
-
async captureGame(opts) {
|
|
356
|
-
const data = await this.command({
|
|
357
|
-
type: "bridge-screenshot",
|
|
358
|
-
...opts?.refreshStarvedFrame === true ? { refreshStarvedFrame: true } : {}
|
|
359
|
-
});
|
|
360
|
-
const layers = data.layers;
|
|
361
|
-
const flatness = data.flatness;
|
|
362
|
-
return {
|
|
363
|
-
base64: data.base64,
|
|
364
|
-
mimeType: data.mimeType,
|
|
365
|
-
composite: data.composite === true,
|
|
366
|
-
...layers && Number.isInteger(layers.canvases) && Number.isInteger(layers.domOverlays) ? { layers } : {},
|
|
367
|
-
// Pass the pixel-honesty fields through as the page reported them: the
|
|
368
|
-
// warning sentence is written where the pixels are, so nothing here
|
|
369
|
-
// re-derives (or softens) it.
|
|
370
|
-
...flatness && typeof flatness.dominantFraction === "number" ? { flatness } : {},
|
|
371
|
-
...data.loopRecoveryFrame === true ? { loopRecoveryFrame: true } : {},
|
|
372
|
-
// Same pass-through rule: the recorded-run notice is written where the
|
|
373
|
-
// pixels are, so nothing here re-derives or softens it.
|
|
374
|
-
...data.recording && typeof data.recording.notice === "string" ? { recording: data.recording } : {}
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
/** Start recording the same clean running-game composite `captureGame`
|
|
378
|
-
* photographs. Recording state lives in the editor page, so another process
|
|
379
|
-
* may stop it later through the same project session. */
|
|
380
|
-
async startGameplayRecording(options = {}) {
|
|
381
|
-
return this.command({
|
|
382
|
-
type: "bridge-recording-start",
|
|
383
|
-
...options.fps !== void 0 ? { fps: options.fps } : {},
|
|
384
|
-
...options.name !== void 0 ? { name: options.name } : {},
|
|
385
|
-
...options.format !== void 0 ? { format: options.format } : {}
|
|
386
|
-
});
|
|
387
|
-
}
|
|
388
|
-
/** Export a paused run as fixed-step video. Advances game state; maximum
|
|
389
|
-
* five minutes. `audio` describes the muxed track, or is `false` when the
|
|
390
|
-
* world implements no `AudioAdapter.renderOffline` and the file is
|
|
391
|
-
* genuinely silent — read it, never assume either. */
|
|
392
|
-
async exportGameplayVideo(options) {
|
|
393
|
-
return this.command(
|
|
394
|
-
{ type: "bridge-recording-export", ...options },
|
|
395
|
-
{ deadlineMs: 61e4, retryTransport: false }
|
|
396
|
-
);
|
|
397
|
-
}
|
|
398
|
-
/** Stop the page-owned recorder and return its WebM path and metadata. */
|
|
399
|
-
async stopGameplayRecording() {
|
|
400
|
-
return this.command({ type: "bridge-recording-stop" });
|
|
401
|
-
}
|
|
402
|
-
/** Read the active capture's monotonic media position. This is the only clock
|
|
403
|
-
* suitable for selecting intervals inside the finalized recording. */
|
|
404
|
-
async getGameplayRecordingTimeline() {
|
|
405
|
-
const timeline = await this.command({
|
|
406
|
-
type: "bridge-recording-timeline"
|
|
407
|
-
});
|
|
408
|
-
return { startedAt: timeline.startedAt, elapsedMs: timeline.elapsedMs };
|
|
409
|
-
}
|
|
410
|
-
/** Encode a recorded canvas/DOM interval into a normal composite WebM. */
|
|
411
|
-
async exportGameplayReplay(options) {
|
|
412
|
-
return this.command(
|
|
413
|
-
{ type: "bridge-recording-replay-export", ...options },
|
|
414
|
-
{ deadlineMs: 61e4, retryTransport: false }
|
|
415
|
-
);
|
|
416
|
-
}
|
|
417
|
-
async captureGameplayReplay(replayPath, positionMs) {
|
|
418
|
-
return this.command({
|
|
419
|
-
type: "bridge-recording-replay-capture",
|
|
420
|
-
replayPath,
|
|
421
|
-
positionMs
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
/**
|
|
425
|
-
* Capture an isolated, deterministic four-view preview through the editor's
|
|
426
|
-
* native Asset Lab. The SDK delegates rendering to the editor; it never
|
|
427
|
-
* loads, clones, or interprets Three.js assets itself.
|
|
428
|
-
*/
|
|
429
|
-
async captureAssetPreview(source, options = {}) {
|
|
430
|
-
const data = await this.command(
|
|
431
|
-
{
|
|
432
|
-
type: "capture-asset-preview",
|
|
433
|
-
...source,
|
|
434
|
-
...options
|
|
435
|
-
},
|
|
436
|
-
// Photographing changes nothing, and this is the relay the editor's `screenshot
|
|
437
|
-
// <module>` command and `project.bake.preview` ride — the lane where a momentary
|
|
438
|
-
// transport failure cost a cold agent three probe modules.
|
|
439
|
-
{ retryTransport: true }
|
|
440
|
-
);
|
|
441
|
-
return {
|
|
442
|
-
width: data.width,
|
|
443
|
-
height: data.height,
|
|
444
|
-
// Absent from editors that predate orientation reporting.
|
|
445
|
-
...data.orientation ? { orientation: data.orientation } : {},
|
|
446
|
-
views: data.views,
|
|
447
|
-
contactSheet: data.contactSheet
|
|
448
|
-
};
|
|
449
|
-
}
|
|
450
|
-
/**
|
|
451
|
-
* A project-defined labeled shot set (the editor's `screenshot <target> --shots <set>` command):
|
|
452
|
-
* the DEFINITION travels with the command (project data — see
|
|
453
|
-
* `AssetPreviewShotSetDefinition`; the CLI resolves it from the registered
|
|
454
|
-
* `project.<set>.previewShots` tool), and the editor's generic
|
|
455
|
-
* capture engine renders it — see `packages/editor-threejs/src/kit/asset-preview.ts`'s
|
|
456
|
-
* `captureShotSetAssetPreview`. Throws (via `command`'s `{ok:false}`
|
|
457
|
-
* unwrap) with a clear message naming the missing joint(s) when the asset
|
|
458
|
-
* lacks a bone the definition requires.
|
|
459
|
-
*/
|
|
460
|
-
async captureShotSetPreview(source, definition, options = {}) {
|
|
461
|
-
const data = await this.command(
|
|
462
|
-
{
|
|
463
|
-
type: "capture-asset-preview",
|
|
464
|
-
...source,
|
|
465
|
-
...options,
|
|
466
|
-
shotSet: definition
|
|
467
|
-
},
|
|
468
|
-
{ retryTransport: true }
|
|
469
|
-
);
|
|
470
|
-
return {
|
|
471
|
-
width: data.width,
|
|
472
|
-
height: data.height,
|
|
473
|
-
shots: data.shots,
|
|
474
|
-
// An older editor predates the empty-frame guard and sends none.
|
|
475
|
-
warnings: data.warnings ?? [],
|
|
476
|
-
contactSheet: data.contactSheet
|
|
477
|
-
};
|
|
478
|
-
}
|
|
479
|
-
/**
|
|
480
|
-
* B8.4 — score the asset against a reference GLB (the editor's `screenshot
|
|
481
|
-
* <model.glb> --compare <ref.glb>` command): matched orthographic front + side silhouettes
|
|
482
|
-
* (equal-height bounding-box framing, both yaw-normalized to face the
|
|
483
|
-
* camera), per-view IoU numbers, and overlay evidence images. The
|
|
484
|
-
* reference GLB's raw bytes travel base64 in the command; the editor
|
|
485
|
-
* renders and scores — the SDK never interprets Three.js assets itself.
|
|
486
|
-
*/
|
|
487
|
-
async captureAssetComparePreview(source, refGlbBase64, options = {}) {
|
|
488
|
-
const { refForward, ...dimensions } = options;
|
|
489
|
-
const data = await this.command({
|
|
490
|
-
type: "capture-asset-preview",
|
|
491
|
-
...source,
|
|
492
|
-
...dimensions,
|
|
493
|
-
compare: { glbBase64: refGlbBase64, ...refForward ? { forward: refForward } : {} }
|
|
494
|
-
});
|
|
495
|
-
return { width: data.width, height: data.height, views: data.views };
|
|
496
|
-
}
|
|
497
|
-
/**
|
|
498
|
-
* The STORY lane (the editor's `screenshot <file>.stories.tsx` command): every CSF export of
|
|
499
|
-
* one project story file rendered in the live session's DOM and captured
|
|
500
|
-
* through the same composite leg {@link captureGame} uses, returned as
|
|
501
|
-
* per-export images plus one variant sheet. `options.story` narrows to a
|
|
502
|
-
* single export.
|
|
503
|
-
*
|
|
504
|
-
* Rendering happens in the EDITOR — the SDK never imports, composes or
|
|
505
|
-
* mounts a CSF module itself; the session already owns that machinery for
|
|
506
|
-
* its Stories panel and this drives it.
|
|
507
|
-
*/
|
|
508
|
-
async captureStoryVariants(modulePath, options = {}) {
|
|
509
|
-
const data = await this.command({
|
|
510
|
-
type: "capture-story-variants",
|
|
511
|
-
modulePath,
|
|
512
|
-
...options
|
|
513
|
-
});
|
|
514
|
-
return {
|
|
515
|
-
modulePath: data.modulePath,
|
|
516
|
-
width: data.width,
|
|
517
|
-
height: data.height,
|
|
518
|
-
variants: data.variants,
|
|
519
|
-
contactSheet: data.contactSheet
|
|
520
|
-
};
|
|
521
|
-
}
|
|
522
|
-
// --- Panels ---
|
|
523
|
-
async showViewport(tab) {
|
|
524
|
-
await this.command({ type: "viewport-tab", tab });
|
|
525
|
-
}
|
|
526
|
-
/** Focus a static workspace panel by the key the editor's panel registry
|
|
527
|
-
* holds; an unknown key refuses naming the keys it does hold. */
|
|
528
|
-
async showPanel(panel) {
|
|
529
|
-
await this.command({ type: "show-panel", panel });
|
|
530
|
-
}
|
|
531
|
-
/** Show several instances of the running game split-screen — multiplayer
|
|
532
|
-
* authoring. Pass a total `count` (default "Player N" labels) or an array of
|
|
533
|
-
* `names` (its length is the count; index 0 is the primary). Requires a live
|
|
534
|
-
* play session. */
|
|
535
|
-
async setInstanceCount(countOrNames) {
|
|
536
|
-
await this.command(
|
|
537
|
-
Array.isArray(countOrNames) ? { type: "set-instance-count", names: countOrNames } : { type: "set-instance-count", count: countOrNames }
|
|
538
|
-
);
|
|
539
|
-
}
|
|
540
|
-
async openAsset(path, kind) {
|
|
541
|
-
await this.command({ type: "open-asset-tab", path, kind });
|
|
542
|
-
}
|
|
543
|
-
/** SELECT a project asset — the other half of the browser's
|
|
544
|
-
* selection-vs-open contract (single click selects and fills the
|
|
545
|
-
* Inspector; double click opens a document). */
|
|
546
|
-
async selectAsset(path) {
|
|
547
|
-
await this.command({ type: "select-asset", path });
|
|
548
|
-
}
|
|
549
|
-
async closeAsset(key) {
|
|
550
|
-
await this.command({ type: "close-asset-tab", key });
|
|
551
|
-
}
|
|
552
|
-
async toggleCommandPalette() {
|
|
553
|
-
await this.command({ type: "toggle-command-palette" });
|
|
554
|
-
}
|
|
555
|
-
async toggleConsole() {
|
|
556
|
-
await this.command({ type: "toggle-console" });
|
|
557
|
-
}
|
|
558
|
-
/** Switch the editor's NAMED WORKSPACE — the task-named layout memory
|
|
559
|
-
* (`game`/`model`/`sculpt`/`texture`/`animate`/`look`). Resolves once the
|
|
560
|
-
* dock has finished rebuilding, so a following capture photographs the
|
|
561
|
-
* arrangement that was asked for. */
|
|
562
|
-
async setWorkspace(workspace) {
|
|
563
|
-
await this.command({ type: "set-workspace", workspace });
|
|
564
|
-
}
|
|
565
|
-
/** Apply a STYLE BUNDLE — palette, material, icon set and region defaults
|
|
566
|
-
* in one gesture (`classic`/`glass`/`maya`/`substance`, or one a package
|
|
567
|
-
* the project or product composes carries, such as `blender`). */
|
|
568
|
-
async setStyle(style) {
|
|
569
|
-
await this.command({ type: "set-style", style });
|
|
570
|
-
}
|
|
571
|
-
/** A document stage's viewport PRESENTATION (`kit/viewport-presentation`), resolved; with a
|
|
572
|
-
* `layer`, that choice is recorded for the view first, as the toolbar records it. */
|
|
573
|
-
/** A view's presentation, answered resolved. With a LAYER the person's choice is recorded
|
|
574
|
-
* first; with a STRING, the named view (`*.view.ts`) of that id is put on the view whole. */
|
|
575
|
-
async viewportPresentation(documentId, layer) {
|
|
576
|
-
return this.command({
|
|
577
|
-
type: "viewport-presentation",
|
|
578
|
-
documentId,
|
|
579
|
-
...typeof layer === "string" ? { preset: layer } : layer ? { layer } : {}
|
|
580
|
-
});
|
|
581
|
-
}
|
|
582
|
-
/** Set the MATERIAL apart from the bundle that usually carries it.
|
|
583
|
-
* Answers with what the chrome wears afterwards. */
|
|
584
|
-
async setAppearance(appearance) {
|
|
585
|
-
return this.command({
|
|
586
|
-
type: "set-appearance",
|
|
587
|
-
...appearance
|
|
588
|
-
});
|
|
589
|
-
}
|
|
590
|
-
async showBuild() {
|
|
591
|
-
await this.command({ type: "show-build" });
|
|
592
|
-
}
|
|
593
|
-
/** Atomically present a durable editor view and return its shareable URL. */
|
|
594
|
-
async present(view) {
|
|
595
|
-
const presented = await this.command({ type: "present-view", view });
|
|
596
|
-
return { view: presented.view, url: presented.url, warnings: presented.warnings };
|
|
597
|
-
}
|
|
598
|
-
/**
|
|
599
|
-
* The INSPECTION SUBJECT the editor is showing right now, as data — the
|
|
600
|
-
* serialized projection of the inspection model (design:
|
|
601
|
-
* `docs/ARCHITECTURE-CORE.md` §Editor chrome, "The Inspection Model").
|
|
602
|
-
*
|
|
603
|
-
* The same subject a human reads in the inspector: identity, presentation,
|
|
604
|
-
* verbs, and the identified sections in display order — with a `fields`
|
|
605
|
-
* section's CURRENT VALUES read through the same io the field rows edit
|
|
606
|
-
* through. With nothing selected it answers the active surface's own
|
|
607
|
-
* no-selection subject when it has one, exactly as the panel does; it never
|
|
608
|
-
* reports another surface's, and when the panel itself is unmounted it
|
|
609
|
-
* answers `{none: true}` rather than a subject nobody is looking at. A
|
|
610
|
-
* `custom` section body is a named opaque (`{kind, id, title}`) — the editor
|
|
611
|
-
* renders those with React — plus its displayed values under `data` when it
|
|
612
|
-
* has any (the Transform section's position/rotation/scale).
|
|
613
|
-
*/
|
|
614
|
-
async inspect() {
|
|
615
|
-
const data = await this.command({ type: "inspect" });
|
|
616
|
-
return data.subject;
|
|
617
|
-
}
|
|
618
|
-
/** Run one verb exposed by the active Inspector subject, by its id. */
|
|
619
|
-
async runInspectionAction(actionId) {
|
|
620
|
-
const data = await this.command({
|
|
621
|
-
type: "run-inspection-action",
|
|
622
|
-
actionId
|
|
623
|
-
});
|
|
624
|
-
return data.subject;
|
|
625
|
-
}
|
|
626
|
-
/**
|
|
627
|
-
* Run ONE command by id — the door to everything the command palette lists.
|
|
628
|
-
*
|
|
629
|
-
* Under the Code-OSS frame this is the workbench's own `ICommandService`, so
|
|
630
|
-
* any command id works: a view's `volter.<view>.<verb>`, an editor action's
|
|
631
|
-
* `volter.action.<id>`, or one of VS Code's own. Standalone the editor's `edit` command has no
|
|
632
|
-
* command service and answers the `volter.<view>.<verb>` shape directly off
|
|
633
|
-
* the views registry, refusing anything else BY NAME.
|
|
634
|
-
*
|
|
635
|
-
* The result is whatever the command answered — a view verb's state, or
|
|
636
|
-
* `null` for a command that returns nothing.
|
|
637
|
-
*/
|
|
638
|
-
async runCommand(commandId, args) {
|
|
639
|
-
const data = await this.command({
|
|
640
|
-
type: "run-command",
|
|
641
|
-
commandId,
|
|
642
|
-
...args === void 0 ? {} : { args }
|
|
643
|
-
});
|
|
644
|
-
return data.result;
|
|
645
|
-
}
|
|
646
|
-
/**
|
|
647
|
-
* One STRUCTURE op on the authored tree — the hierarchy context menu's own
|
|
648
|
-
* verbs, on the same helpers, for a caller with no pointer to right-click
|
|
649
|
-
* with. `id`/`ids` default to the current selection.
|
|
650
|
-
*/
|
|
651
|
-
async structureOp(op, options = {}) {
|
|
652
|
-
return this.command({ type: "structure-op", op, ...options });
|
|
653
|
-
}
|
|
654
|
-
/** "Extract Component…" — the hierarchy row's action, as a command. Answers
|
|
655
|
-
* the action's own sentence, which NAMES the files it created. */
|
|
656
|
-
async extractComponent(options = {}) {
|
|
657
|
-
return this.command({ type: "extract-component", ...options });
|
|
658
|
-
}
|
|
659
|
-
/** "Fork Component…" — extract's twin: one new file, one callsite retargeted. */
|
|
660
|
-
async forkComponent(options = {}) {
|
|
661
|
-
return this.command({ type: "fork-component", ...options });
|
|
662
|
-
}
|
|
663
|
-
/**
|
|
664
|
-
* The HIERARCHY PANEL's actual rendered row tree, as data.
|
|
665
|
-
*
|
|
666
|
-
* The same rows a human is looking at: the adapter's tree after the component
|
|
667
|
-
* marks fold implementation subtrees, after the internals reveal, after the
|
|
668
|
-
* document promotion, the child cap, the collapse state, the search filter
|
|
669
|
-
* and the selection scope. Works in play mode and edit mode alike — the
|
|
670
|
-
* answer reports which (`playState`, `activeViewportTab`), because a
|
|
671
|
-
* play-mode tree and an edit-mode tree come from different adapters.
|
|
672
|
-
*
|
|
673
|
-
* Deliberately NOT `status().entities`, which walks the raw adapter tree and
|
|
674
|
-
* therefore answers a different question: a panel defect is invisible in it.
|
|
675
|
-
*
|
|
676
|
-
* Each row carries `childCount` (what its caret opens), `internalChildCount`
|
|
677
|
-
* (what is folded behind "Reveal Internals") and `expandable` (whether the
|
|
678
|
-
* panel draws a caret at all) — so "this subtree exists but the UI offers no
|
|
679
|
-
* way to open it" is a readable fact rather than something only a human
|
|
680
|
-
* squinting at the panel can notice.
|
|
681
|
-
*
|
|
682
|
-
* Rejects, naming the panel, when no hierarchy panel is mounted: an empty
|
|
683
|
-
* tree would be a fabricated answer about a surface nobody is being shown.
|
|
684
|
-
*/
|
|
685
|
-
async hierarchy() {
|
|
686
|
-
const data = await this.command({ type: "hierarchy" });
|
|
687
|
-
return data.hierarchy;
|
|
688
|
-
}
|
|
689
|
-
/** Run the Hierarchy panel's own Expand All action. */
|
|
690
|
-
async expandHierarchyAll() {
|
|
691
|
-
await this.command({ type: "expand-hierarchy-all" });
|
|
692
|
-
}
|
|
693
|
-
/** Run the Hierarchy panel's own Collapse All action — Expand All's other
|
|
694
|
-
* half, and the only way back to the tree's rest state through the product
|
|
695
|
-
* (see `HierarchyPanelSnapshot.collapseAll`). */
|
|
696
|
-
async collapseHierarchyAll() {
|
|
697
|
-
await this.command({ type: "collapse-hierarchy-all" });
|
|
698
|
-
}
|
|
699
|
-
/**
|
|
700
|
-
* Write one editable path through the active Inspector's own IO.
|
|
701
|
-
*
|
|
702
|
-
* The answer carries `write` as well as the subject, because an ack alone
|
|
703
|
-
* cannot be believed: a write with no persistence route open succeeds and
|
|
704
|
-
* changes no byte, and `write.persisted` is how the caller tells the two
|
|
705
|
-
* apart without diffing the tree (`InspectedWriteDestination` in `types.ts`).
|
|
706
|
-
*/
|
|
707
|
-
async setInspectionField(path, value) {
|
|
708
|
-
const data = await this.command({
|
|
709
|
-
type: "set-inspection-field",
|
|
710
|
-
path,
|
|
711
|
-
value
|
|
712
|
-
});
|
|
713
|
-
return { subject: data.subject, write: data.write };
|
|
714
|
-
}
|
|
715
|
-
/**
|
|
716
|
-
* REMOVE one editable path's authored override — the other half of the write
|
|
717
|
-
* door, and the only one that can express byte-ABSENCE.
|
|
718
|
-
*
|
|
719
|
-
* {@link setInspectionField} writes a VALUE, so reverting a property an
|
|
720
|
-
* authoring gesture ADDED puts the default back EXPLICITLY and leaves the
|
|
721
|
-
* source one attribute heavier than it started. This drops the property, so
|
|
722
|
-
* whatever governs it in its absence takes over — the same `io.remove` the
|
|
723
|
-
* Inspector's revert arrow calls, the same persistence pipe, the same awaited
|
|
724
|
-
* `{ destination, persisted }` ack.
|
|
725
|
-
*
|
|
726
|
-
* Rejects with `code: 'REMOVAL_UNAVAILABLE'` when the field does not declare
|
|
727
|
-
* itself removable or the lane implements no removal door. That refusal is a
|
|
728
|
-
* MISSING SEAM, not a failed removal, and it is coded rather than phrased
|
|
729
|
-
* precisely so a caller can grade the two differently.
|
|
730
|
-
*/
|
|
731
|
-
async removeInspectionField(path) {
|
|
732
|
-
const data = await this.command({
|
|
733
|
-
type: "remove-inspection-field",
|
|
734
|
-
path
|
|
735
|
-
});
|
|
736
|
-
return { subject: data.subject, write: data.write };
|
|
737
|
-
}
|
|
738
|
-
/**
|
|
739
|
-
* OPEN one piece of the adapter's SCENE TABLE by id — a scene, a prefab, or
|
|
740
|
-
* a story state, because the table makes them siblings (they differ only in
|
|
741
|
-
* instance site). The ids are exactly what `getState().adapter.scenes.entries`
|
|
742
|
-
* reports, so the table is both the menu and the address space.
|
|
743
|
-
*
|
|
744
|
-
* With a game LIVE in the session, opening a scene the adapter declares
|
|
745
|
-
* reachable through that game's own scenes contract NAVIGATES it — the same
|
|
746
|
-
* switch the editor's own scene picker makes — and the answer carries the
|
|
747
|
-
* game's own reading (`scene`).
|
|
748
|
-
*
|
|
749
|
-
* Rejects with a coded reason rather than prose: `SCENE_NOT_FOUND` (and it
|
|
750
|
-
* names the ids that DO exist), `SCENE_NOT_OPENABLE` carrying the adapter's
|
|
751
|
-
* own declared reason for a scene it says nothing can reach,
|
|
752
|
-
* `SCENE_NAVIGATION_NOT_RUNNING` for a live-only scene with no game running,
|
|
753
|
-
* `SCENE_CONTRACT_UNAVAILABLE` / `SCENE_NOT_IN_CONTRACT` (naming the ids the
|
|
754
|
-
* game itself publishes) / `SCENE_SWITCH_FAILED` when the running game's own
|
|
755
|
-
* navigation cannot take it, `SCENE_NOT_OPENABLE_LIVE` when this session has
|
|
756
|
-
* no remount for a native swap-slot scene,
|
|
757
|
-
* `SCENE_TABLE_UNAVAILABLE` before the adapter has loaded, and
|
|
758
|
-
* `SCENE_DOCUMENT_NOT_MOUNTED` when the host has no document for a piece the
|
|
759
|
-
* table says is openable — a host gap, not a table statement.
|
|
760
|
-
*/
|
|
761
|
-
async open(id) {
|
|
762
|
-
return this.command({ type: "open", id });
|
|
763
|
-
}
|
|
764
|
-
/**
|
|
765
|
-
* Undo / redo one project transaction — the same queue the keyboard shortcut
|
|
766
|
-
* drives. `moved` is false when there was nothing left in that direction.
|
|
767
|
-
*/
|
|
768
|
-
async undo() {
|
|
769
|
-
return this.command({ type: "undo" });
|
|
770
|
-
}
|
|
771
|
-
async redo() {
|
|
772
|
-
return this.command({ type: "redo" });
|
|
773
|
-
}
|
|
774
|
-
/** Read the editor's actual current durable projection. */
|
|
775
|
-
async currentView() {
|
|
776
|
-
const data = await this.command({ type: "current-view" });
|
|
777
|
-
return data.view;
|
|
778
|
-
}
|
|
779
|
-
/**
|
|
780
|
-
* Capture the active center document exactly as presented to the user.
|
|
781
|
-
*
|
|
782
|
-
* A number is a SQUARE of that size (the default shape); `{width, height}`
|
|
783
|
-
* asks for a shaped frame — a video-aspect look that needs no crop. Both are
|
|
784
|
-
* bounded by the relay budget; see {@link CaptureDimensions}.
|
|
785
|
-
*/
|
|
786
|
-
/** Photograph the editor PAGE itself — every panel as the person sees it, at
|
|
787
|
-
* `scale` output pixels per CSS pixel (default `devicePixelRatio`), which is
|
|
788
|
-
* what a 1 px border or a glyph edge is judged through. */
|
|
789
|
-
async captureEditorChrome(options) {
|
|
790
|
-
return this.command({
|
|
791
|
-
type: "capture-editor-chrome",
|
|
792
|
-
...options?.scale === void 0 ? {} : { scale: options.scale },
|
|
793
|
-
...options?.region === void 0 ? {} : { region: options.region },
|
|
794
|
-
...options?.name === void 0 ? {} : { name: options.name }
|
|
795
|
-
});
|
|
796
|
-
}
|
|
797
|
-
/** With a view, present and capture it in one request so document discovery
|
|
798
|
-
* cannot retarget the capture between two client calls. */
|
|
799
|
-
async captureActiveDocument(size, view) {
|
|
800
|
-
return this.command({
|
|
801
|
-
type: "capture-active-document",
|
|
802
|
-
...view ? { view } : {},
|
|
803
|
-
...typeof size === "number" ? { size } : {},
|
|
804
|
-
...typeof size === "object" && size !== null ? { width: size.width, height: size.height } : {}
|
|
805
|
-
});
|
|
806
|
-
}
|
|
807
|
-
/**
|
|
808
|
-
* Read or drive the ACTIVE center document's own DOM — the scoped
|
|
809
|
-
* editor-chrome door, and the read/gesture half of the same subject
|
|
810
|
-
* {@link captureActiveDocument} photographs. NOT play-mode gated, and NOT
|
|
811
|
-
* page automation: a target outside the active document's container is
|
|
812
|
-
* refused by name. Design and scope contract:
|
|
813
|
-
* `packages/sdk/src/kit/editor-document-probe.ts`.
|
|
814
|
-
*/
|
|
815
|
-
async documentProbe(step) {
|
|
816
|
-
return this.command({ type: "document-probe", step });
|
|
817
|
-
}
|
|
818
|
-
/**
|
|
819
|
-
* Run a wire-carried step against the ACTIVE document's published context
|
|
820
|
-
* (`packages/sdk/src/kit/document-context-registry.ts`) — the REPL door over
|
|
821
|
-
* an open document, in Edit mode. `src` is the step's own `toString()`;
|
|
822
|
-
* same serialization contract as `page-script` (no closures survive).
|
|
823
|
-
*/
|
|
824
|
-
/**
|
|
825
|
-
* The Blender lane's doors (`blender-execute`, `blender-scene-info`,
|
|
826
|
-
* `blender-object-info`, `blender-screenshot-view`, `blender-read-file`,
|
|
827
|
-
* `blender-write-file`, `blender-list-files`, `blender-start`,
|
|
828
|
-
* `blender-status`): Blender runs in the editor tab's worker, and
|
|
829
|
-
* `volter blender-mcp` is transport onto these. `blender-status` is the only
|
|
830
|
-
* one that creates nothing — it answers whether this tab already has a
|
|
831
|
-
* session, which is how a caller survives an editor restart.
|
|
832
|
-
*/
|
|
833
|
-
async blender(type, fields = {}) {
|
|
834
|
-
return this.command({ type, ...fields }, { deadlineMs: BLENDER_DEADLINE_MS });
|
|
835
|
-
}
|
|
836
|
-
/**
|
|
837
|
-
* Model Play's log (`model-play-log`, contributed by `@volter/play`): what the
|
|
838
|
-
* running play script logged with `play.log`, and the runner's lifecycle entries, stamped
|
|
839
|
-
* with simulation time and frame. `documentId` names the model document (the active Play's
|
|
840
|
-
* when omitted); `since` keeps entries at or after that many simulation seconds; `kind`
|
|
841
|
-
* keeps one kind.
|
|
842
|
-
*/
|
|
843
|
-
async modelPlayLog(query = {}) {
|
|
844
|
-
const { ok: _ok, ...reading } = await this.command({ type: "model-play-log", ...query });
|
|
845
|
-
return reading;
|
|
846
|
-
}
|
|
847
|
-
async documentScript(src) {
|
|
848
|
-
const outcome = await this.command({ type: "document-script", src });
|
|
849
|
-
return outcome.result;
|
|
850
|
-
}
|
|
851
|
-
// --- Display (set semantics) ---
|
|
852
|
-
async setGrid(enabled) {
|
|
853
|
-
await this.command({ type: "set-grid", enabled });
|
|
854
|
-
}
|
|
855
|
-
async setHelpers(enabled) {
|
|
856
|
-
await this.command({ type: "set-helpers", enabled });
|
|
857
|
-
}
|
|
858
|
-
async setStats(enabled) {
|
|
859
|
-
await this.command({ type: "set-stats", enabled });
|
|
860
|
-
}
|
|
861
|
-
async setShadingMode(mode) {
|
|
862
|
-
await this.command({ type: "set-shading-mode", mode });
|
|
863
|
-
}
|
|
864
|
-
async setHelperType(helperType, enabled) {
|
|
865
|
-
await this.command({ type: "set-helper-type", helperType, enabled });
|
|
866
|
-
}
|
|
867
|
-
// --- Transform tools (set semantics) ---
|
|
868
|
-
async setTransformMode(mode) {
|
|
869
|
-
await this.command({ type: "set-transform-mode", mode });
|
|
870
|
-
}
|
|
871
|
-
async setTransformSpace(space) {
|
|
872
|
-
await this.command({ type: "set-transform-space", space });
|
|
873
|
-
}
|
|
874
|
-
async setSnap(enabled) {
|
|
875
|
-
await this.command({ type: "set-snap", enabled });
|
|
876
|
-
}
|
|
877
|
-
// --- Project management ---
|
|
878
|
-
async createProject(name, location, template = "default", exampleId) {
|
|
879
|
-
const res = await this.httpFetch(`${this.baseUrl}/__editor/create-project`, {
|
|
880
|
-
method: "POST",
|
|
881
|
-
headers: { "Content-Type": "application/json" },
|
|
882
|
-
body: JSON.stringify({ name, location, template, ...exampleId ? { exampleId } : {} })
|
|
883
|
-
});
|
|
884
|
-
const data = await this.readJson(res);
|
|
885
|
-
if (!res.ok) {
|
|
886
|
-
throw new Error(data.error ?? `Create project failed: ${res.status}`);
|
|
887
|
-
}
|
|
888
|
-
return { path: data.path, config: data.config };
|
|
889
|
-
}
|
|
890
|
-
async openProject(path) {
|
|
891
|
-
const res = await this.httpFetch(`${this.baseUrl}/__editor/open-project`, {
|
|
892
|
-
method: "POST",
|
|
893
|
-
headers: { "Content-Type": "application/json" },
|
|
894
|
-
body: JSON.stringify({ path })
|
|
895
|
-
});
|
|
896
|
-
const body = await this.readJson(res);
|
|
897
|
-
if (!res.ok) {
|
|
898
|
-
throw new Error(body.error ?? `Open project failed: ${res.status}`);
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
async getProject() {
|
|
902
|
-
const res = await this.httpFetch(`${this.baseUrl}/__editor/project`);
|
|
903
|
-
const data = await this.readJson(res);
|
|
904
|
-
if (!res.ok) throw new Error(data.error ?? `Failed to get project: ${res.status}`);
|
|
905
|
-
return data.project;
|
|
906
|
-
}
|
|
907
|
-
async listRecentProjects() {
|
|
908
|
-
const res = await this.httpFetch(`${this.baseUrl}/__editor/recent-projects`);
|
|
909
|
-
const data = await this.readJson(res);
|
|
910
|
-
if (!res.ok) throw new Error(data.error ?? `Failed to list projects: ${res.status}`);
|
|
911
|
-
return data.projects;
|
|
912
|
-
}
|
|
913
|
-
// --- Registered project tools ---
|
|
914
|
-
/** List tools explicitly registered in `package.json#volter.tools`.
|
|
915
|
-
* The editor server loads callable metadata in Node; modules never enter the
|
|
916
|
-
* editor browser merely because they were listed. */
|
|
917
|
-
async listProjectTools() {
|
|
918
|
-
const res = await this.httpFetch(`${this.baseUrl}/__editor/project-tools`);
|
|
919
|
-
const body = await this.readJson(res);
|
|
920
|
-
if (!res.ok) throw new Error(`Failed to list project tools: ${res.status}`);
|
|
921
|
-
return body;
|
|
922
|
-
}
|
|
923
|
-
/** Execute one Node-hosted project tool through the shared validated
|
|
924
|
-
* dispatcher. Write/destructive tools require `confirm:true`. */
|
|
925
|
-
async runProjectTool(name, input = {}, options = {}) {
|
|
926
|
-
const dispatcher = createDispatcher(0);
|
|
927
|
-
try {
|
|
928
|
-
const res = await this.httpFetch(
|
|
929
|
-
`${this.baseUrl}/__editor/project-tools/run`,
|
|
930
|
-
{
|
|
931
|
-
method: "POST",
|
|
932
|
-
headers: { "Content-Type": "application/json" },
|
|
933
|
-
body: JSON.stringify({
|
|
934
|
-
name,
|
|
935
|
-
input,
|
|
936
|
-
confirm: options.confirm === true,
|
|
937
|
-
// Omitted (not null) when unset — the wire body is JSON and the tool
|
|
938
|
-
// host reads absence as "the sole instance", same convention as the
|
|
939
|
-
// relay's `instance`.
|
|
940
|
-
...options.instance !== void 0 ? { instance: options.instance } : {}
|
|
941
|
-
})
|
|
942
|
-
},
|
|
943
|
-
dispatcher
|
|
944
|
-
);
|
|
945
|
-
const body = await this.readJson(res);
|
|
946
|
-
if (!body || typeof body !== "object" || typeof body.ok !== "boolean") {
|
|
947
|
-
throw new Error(`Project tool returned an invalid response (${res.status}).`);
|
|
948
|
-
}
|
|
949
|
-
return body;
|
|
950
|
-
} finally {
|
|
951
|
-
await dispatcher?.destroy?.();
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
// --- First-party generation job activity ---
|
|
955
|
-
/** Read the one project-local generation job ledger. Provider-native
|
|
956
|
-
* request/result shapes remain on their registered operations. */
|
|
957
|
-
async listGenerationJobs() {
|
|
958
|
-
const res = await this.httpFetch(`${this.baseUrl}/__editor/generations`);
|
|
959
|
-
const body = await this.readJson(res);
|
|
960
|
-
if (!res.ok) throw new Error(`Failed to list generation jobs: ${res.status}`);
|
|
961
|
-
return body;
|
|
962
|
-
}
|
|
963
|
-
/** Forget operational job state. Accepted provenance and project assets
|
|
964
|
-
* are deliberately unaffected. */
|
|
965
|
-
async forgetGenerationJob(id) {
|
|
966
|
-
const res = await this.httpFetch(
|
|
967
|
-
`${this.baseUrl}/__editor/generations/${encodeURIComponent(id)}`,
|
|
968
|
-
{
|
|
969
|
-
method: "DELETE"
|
|
970
|
-
}
|
|
971
|
-
);
|
|
972
|
-
const body = await this.readJson(res);
|
|
973
|
-
if (!res.ok) throw new Error(body.error ?? `Failed to forget generation job: ${res.status}`);
|
|
974
|
-
return body.removed === true;
|
|
975
|
-
}
|
|
976
|
-
// --- Logs ---
|
|
977
|
-
async getLogEntries() {
|
|
978
|
-
const res = await this.httpFetch(`${this.baseUrl}/__editor/log-entries`);
|
|
979
|
-
const data = await this.readJson(res);
|
|
980
|
-
if (!res.ok) return [];
|
|
981
|
-
return data.entries;
|
|
982
|
-
}
|
|
983
|
-
// --- State ---
|
|
984
|
-
/**
|
|
985
|
-
* The document table the host resolved — every scene, prefab, page, model,
|
|
986
|
-
* shot, take … the project's finders produced (`getState().adapter.scenes`
|
|
987
|
-
* is the same projection). A command, so it answers wherever the control
|
|
988
|
-
* channel reaches, not only where `/__editor/state` is served.
|
|
989
|
-
*/
|
|
990
|
-
async documentTable() {
|
|
991
|
-
return this.command({ type: "document-table" });
|
|
992
|
-
}
|
|
993
|
-
/**
|
|
994
|
-
* Reload the editor's page and wait until the new document answers — the
|
|
995
|
-
* host's `page-reload`, for a product with no game client (`@volter/game-live`'s
|
|
996
|
-
* `page.reload()` is the same order with the same two witnesses). A tab's
|
|
997
|
-
* epoch count rising proves a new document loaded; a host command answering
|
|
998
|
-
* proves it can be driven. It is sent only once that tab reports its command
|
|
999
|
-
* listener ready, because a command sent before is filed as a stall.
|
|
1000
|
-
*/
|
|
1001
|
-
async reloadPage(timeoutMs = 6e4) {
|
|
1002
|
-
const epochs = async () => {
|
|
1003
|
-
const tabs = (await this.getState().catch(() => ({ tabs: [] }))).tabs ?? [];
|
|
1004
|
-
const byTab = tabs;
|
|
1005
|
-
return new Map(byTab.map((tab) => [tab.tabId8, tab]));
|
|
1006
|
-
};
|
|
1007
|
-
const pause = () => new Promise((settle) => setTimeout(settle, 250));
|
|
1008
|
-
const before = await epochs();
|
|
1009
|
-
const deadline = Date.now() + timeoutMs;
|
|
1010
|
-
await this.command({ type: "page-reload" });
|
|
1011
|
-
let reloaded = [];
|
|
1012
|
-
while (reloaded.length === 0) {
|
|
1013
|
-
if (Date.now() > deadline) throw new Error("The reload was ordered and no tab of this session reported a new page load.");
|
|
1014
|
-
await pause();
|
|
1015
|
-
const now = await epochs();
|
|
1016
|
-
reloaded = [...now].filter(([tabId, tab]) => tab.epochCount > (before.get(tabId)?.epochCount ?? 0)).map(([tabId]) => tabId);
|
|
1017
|
-
}
|
|
1018
|
-
for (; ; ) {
|
|
1019
|
-
if (Date.now() > deadline) throw new Error("A new page loaded, but its command listener did not become ready.");
|
|
1020
|
-
const now = await epochs();
|
|
1021
|
-
if (reloaded.some((tabId) => {
|
|
1022
|
-
const listener = now.get(tabId)?.commandListener;
|
|
1023
|
-
return typeof listener !== "string" || listener === "ready";
|
|
1024
|
-
})) break;
|
|
1025
|
-
await pause();
|
|
1026
|
-
}
|
|
1027
|
-
for (; ; ) {
|
|
1028
|
-
try {
|
|
1029
|
-
await this.command({ type: "current-view" });
|
|
1030
|
-
return;
|
|
1031
|
-
} catch (error) {
|
|
1032
|
-
if (Date.now() > deadline) throw error;
|
|
1033
|
-
await pause();
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
async getState() {
|
|
1038
|
-
const res = await this.httpFetch(`${this.baseUrl}/__editor/state`);
|
|
1039
|
-
const state = await this.readJson(res);
|
|
1040
|
-
if (!res.ok) throw new Error(`Failed to get editor state: ${res.status} ${res.statusText}`);
|
|
1041
|
-
return state;
|
|
1042
|
-
}
|
|
1043
|
-
/**
|
|
1044
|
-
* The complete unresolved console set the session is holding right now.
|
|
1045
|
-
*
|
|
1046
|
-
* Command envelopes only carry COUNTS (`unresolvedConsole` on
|
|
1047
|
-
* `commandResponseFor`). The named conditions live on GET `/__editor/console`.
|
|
1048
|
-
* This is the method that turns "a command that exits before an envelope
|
|
1049
|
-
* arrives" into a real reading: the CLI calls it at start and at exit
|
|
1050
|
-
* through the same {@link onEnvelope} observer every other response uses.
|
|
1051
|
-
* A session that does not answer within {@link CONSOLE_DRAIN_TIMEOUT_MS} is
|
|
1052
|
-
* a thrown error the caller treats as "nothing learned", never a hang.
|
|
1053
|
-
*/
|
|
1054
|
-
async getUnresolvedConsole(opts) {
|
|
1055
|
-
const res = await this.httpFetch(
|
|
1056
|
-
`${this.baseUrl}/__editor/console${opts?.all === true ? "?all=1" : ""}`,
|
|
1057
|
-
{ signal: AbortSignal.timeout(CONSOLE_DRAIN_TIMEOUT_MS) }
|
|
1058
|
-
);
|
|
1059
|
-
if (!res.ok) {
|
|
1060
|
-
throw new Error(`Failed to read unresolved console: ${res.status}`);
|
|
1061
|
-
}
|
|
1062
|
-
return await this.readJson(res, true);
|
|
1063
|
-
}
|
|
1064
|
-
/** Acknowledge one named console condition. The response is observed and
|
|
1065
|
-
* hydrated through the same path as every other client response. */
|
|
1066
|
-
async acknowledgeConsole(input) {
|
|
1067
|
-
const res = await this.httpFetch(`${this.baseUrl}/__editor/console/ack`, {
|
|
1068
|
-
method: "POST",
|
|
1069
|
-
headers: { "Content-Type": "application/json" },
|
|
1070
|
-
body: JSON.stringify(input),
|
|
1071
|
-
signal: AbortSignal.timeout(CONSOLE_DRAIN_TIMEOUT_MS * 4)
|
|
1072
|
-
});
|
|
1073
|
-
const body = await this.readJson(res);
|
|
1074
|
-
return body;
|
|
1075
|
-
}
|
|
1076
|
-
/**
|
|
1077
|
-
* `fetch` for this client's plain routes, with the ONE thing Node's `fetch`
|
|
1078
|
-
* will not do: name why it failed.
|
|
1079
|
-
*
|
|
1080
|
-
* `readJson` below already owns "the server answered the wrong thing"; this
|
|
1081
|
-
* owns "nothing answered at all", which used to reach the caller as the bare
|
|
1082
|
-
* `TypeError: fetch failed` with the real code buried on `.cause`. No retry
|
|
1083
|
-
* here — these routes create projects, run tools and acknowledge console
|
|
1084
|
-
* conditions, so repeating one is the caller's decision. The relayed
|
|
1085
|
-
* `command` path above has its own opt-in retry for the read-only captures.
|
|
1086
|
-
*/
|
|
1087
|
-
async httpFetch(url, init, dispatcher) {
|
|
1088
|
-
try {
|
|
1089
|
-
return this.fetchOverride ? await this.fetchOverride(url, init) : await dispatchFetch(url, init, dispatcher);
|
|
1090
|
-
} catch (error) {
|
|
1091
|
-
if (error?.name === "TimeoutError") throw error;
|
|
1092
|
-
const { code, detail } = describeFetchFailure(error);
|
|
1093
|
-
throw new EditorCommandError(
|
|
1094
|
-
`${init?.method ?? "GET"} ${url} never reached the editor: ${code}${detail ? ` (${detail})` : ""}. Nothing answered on that port \u2014 check the terminal that started this editor session and confirm the port this client resolved.`,
|
|
1095
|
-
code,
|
|
1096
|
-
false
|
|
1097
|
-
);
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
/** Parse one JSON body and hand it to {@link onEnvelope}.
|
|
1101
|
-
*
|
|
1102
|
-
* A command/state envelope carries current counts but not the named set. If
|
|
1103
|
-
* an observer is installed, do the bounded console GET before resolving the
|
|
1104
|
-
* original request. That makes a subsequent `process.exit()` safe: the
|
|
1105
|
-
* observer has already received every condition and occurrence count. */
|
|
1106
|
-
async readJson(res, consoleComplete = false) {
|
|
1107
|
-
const contentType = res.headers.get("content-type") ?? "";
|
|
1108
|
-
if (!contentType.includes("application/json")) {
|
|
1109
|
-
throw new Error(
|
|
1110
|
-
`The editor at ${this.baseUrl} answered with its page fallback (${contentType || "no content-type"}) rather than JSON, so no editor server handled the request. Check that this URL is a running editor session.`
|
|
1111
|
-
);
|
|
1112
|
-
}
|
|
1113
|
-
const body = await res.json();
|
|
1114
|
-
this.observe(body, { unresolvedConsoleComplete: consoleComplete });
|
|
1115
|
-
if (this.onEnvelope !== null && !consoleComplete && body !== null && typeof body === "object" && "unresolvedConsole" in body) {
|
|
1116
|
-
try {
|
|
1117
|
-
const consoleRes = await this.httpFetch(`${this.baseUrl}/__editor/console`, {
|
|
1118
|
-
signal: AbortSignal.timeout(CONSOLE_DRAIN_TIMEOUT_MS)
|
|
1119
|
-
});
|
|
1120
|
-
if (consoleRes.ok) {
|
|
1121
|
-
const consoleBody = await consoleRes.json();
|
|
1122
|
-
this.observe(consoleBody, { unresolvedConsoleComplete: true });
|
|
1123
|
-
}
|
|
1124
|
-
} catch {
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
return body;
|
|
1128
|
-
}
|
|
1129
|
-
/** Hand one response body to {@link onEnvelope}, never letting it throw. */
|
|
1130
|
-
observe(body, observation) {
|
|
1131
|
-
if (this.onEnvelope === null) return;
|
|
1132
|
-
try {
|
|
1133
|
-
this.onEnvelope(body, observation);
|
|
1134
|
-
} catch {
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1137
|
-
/**
|
|
1138
|
-
* Whether an editor browser tab is connected to the server *right now*.
|
|
1139
|
-
* Unlike {@link getState}, this reflects live SSE connections, not cached
|
|
1140
|
-
* state — use it to check whether commands will actually reach an editor.
|
|
1141
|
-
*/
|
|
1142
|
-
async isConnected() {
|
|
1143
|
-
const state = await this.getState();
|
|
1144
|
-
return state.connected === true;
|
|
1145
|
-
}
|
|
1146
|
-
async waitForState(predicate, timeoutMs = 1e4) {
|
|
1147
|
-
const start = Date.now();
|
|
1148
|
-
while (Date.now() - start < timeoutMs) {
|
|
1149
|
-
const state = await this.getState();
|
|
1150
|
-
if (predicate(state)) return state;
|
|
1151
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
1152
|
-
}
|
|
1153
|
-
throw new Error(`waitForState timed out after ${timeoutMs}ms`);
|
|
1154
|
-
}
|
|
1155
|
-
};
|
|
1
|
+
import {
|
|
2
|
+
EditorClient
|
|
3
|
+
} from "./chunk-7SZ3C4CC.js";
|
|
4
|
+
import {
|
|
5
|
+
EDITOR_SESSION_DISCOVERY_TIMEOUT_MS,
|
|
6
|
+
HttpSessionDiscovery,
|
|
7
|
+
withTimeout
|
|
8
|
+
} from "./chunk-USXOIBVX.js";
|
|
9
|
+
import {
|
|
10
|
+
servedProjectAnswer
|
|
11
|
+
} from "./chunk-Y5W77HYT.js";
|
|
12
|
+
import {
|
|
13
|
+
resolveProductForProject
|
|
14
|
+
} from "./chunk-SDIWRX5X.js";
|
|
15
|
+
import "./chunk-7WKVJGHZ.js";
|
|
16
|
+
import "./chunk-YPAOTPIF.js";
|
|
1156
17
|
|
|
1157
18
|
// src/editor-document.ts
|
|
1158
19
|
var LiveEditorDocument = class {
|
|
@@ -1220,9 +81,7 @@ var LiveEditorDocument = class {
|
|
|
1220
81
|
}
|
|
1221
82
|
/**
|
|
1222
83
|
* A real pointer DRAG across one matched element — press at `from`, move,
|
|
1223
|
-
* release at `to`.
|
|
1224
|
-
* zero-length drag is a click at that fraction, which `click` (always the
|
|
1225
|
-
* center) cannot place.
|
|
84
|
+
* release at `to`. A zero-length drag presses and releases at one fraction.
|
|
1226
85
|
*
|
|
1227
86
|
* `from`, `to` and every point in `via` are `[x, y]` FRACTIONS OF THE
|
|
1228
87
|
* MATCHED ELEMENT'S BOX, 0..1 from its top-left — NEVER pixels and never
|
|
@@ -1279,13 +138,10 @@ var LiveEditorDocument = class {
|
|
|
1279
138
|
});
|
|
1280
139
|
}
|
|
1281
140
|
/**
|
|
1282
|
-
*
|
|
1283
|
-
*
|
|
1284
|
-
*
|
|
1285
|
-
*
|
|
1286
|
-
* `ctx.history`, `session.commit()`). Edit mode, no play. Serialized like
|
|
1287
|
-
* `game.page`: the step's own source travels, so inline every value it
|
|
1288
|
-
* needs and return plain data.
|
|
141
|
+
* Run `step` in the editor page against the active document's published
|
|
142
|
+
* context, with its document id in `info`. The document owns the context's
|
|
143
|
+
* API. The step's source is serialized, so inline every value it needs and
|
|
144
|
+
* return plain data.
|
|
1289
145
|
*/
|
|
1290
146
|
async run(step) {
|
|
1291
147
|
return this.#client.documentScript(step.toString());
|
|
@@ -1316,15 +172,8 @@ var EXTENSION_KIND = {
|
|
|
1316
172
|
".glsl": "source",
|
|
1317
173
|
".vert": "source",
|
|
1318
174
|
".frag": "source",
|
|
1319
|
-
//
|
|
1320
|
-
//
|
|
1321
|
-
// viewer (`asset-documents.tsx#assetDocumentViewerRoute`: `spec.kind ===
|
|
1322
|
-
// 'json'` is decided before any content routing), and the LIVE MODELING
|
|
1323
|
-
// DOCUMENT never mounts — `editor.openAsset('src/lib/fox/fox.model.ts')`
|
|
1324
|
-
// silently shows a text pane instead of the model. Only `kind: 'source'`
|
|
1325
|
-
// reaches `SourceAssetViewer`, which is what content-routes a project script
|
|
1326
|
-
// to `LiveModuleDocument`. The set matches that viewer's own
|
|
1327
|
-
// `isProjectScriptPath` regex, `/\.(?:[cm]?[jt]sx?)$/`.
|
|
175
|
+
// Source kinds select SourceAssetViewer, which routes project scripts by content to
|
|
176
|
+
// LiveModuleDocument. Keep these extensions aligned with its isProjectScriptPath predicate.
|
|
1328
177
|
".ts": "source",
|
|
1329
178
|
".tsx": "source",
|
|
1330
179
|
".mts": "source",
|
|
@@ -1341,61 +190,25 @@ function inferAssetKind(path) {
|
|
|
1341
190
|
return EXTENSION_KIND[ext] ?? "json";
|
|
1342
191
|
}
|
|
1343
192
|
var LiveEditor = class {
|
|
1344
|
-
/** `#`-private, not `private`:
|
|
193
|
+
/** `#`-private, not `private`: `<command> eval --list` enumerates this object's
|
|
1345
194
|
* real runtime members, and TypeScript's erased `private` would leave the
|
|
1346
195
|
* raw `EditorClient` advertised beside them. */
|
|
1347
196
|
#client;
|
|
1348
|
-
/**
|
|
1349
|
-
*
|
|
1350
|
-
*
|
|
1351
|
-
* deliberately scoped to that document alone —
|
|
1352
|
-
* `packages/sdk/src/kit/editor-document-probe.ts` carries the design and the
|
|
1353
|
-
* refusal contract. Screenshotting the same subject is
|
|
1354
|
-
* {@link LiveEditor.captureActiveDocument}, not a fifth verb here.
|
|
1355
|
-
*/
|
|
197
|
+
/** Read and drive DOM within a named scope: the active document by default, or editor chrome
|
|
198
|
+
* such as header, shelf, rail, outliner, utility and area. Targets outside the scope are refused.
|
|
199
|
+
* See `@volter/sdk`'s editor-document-probe for scope and gesture contracts. */
|
|
1356
200
|
document;
|
|
1357
201
|
constructor(client) {
|
|
1358
202
|
this.#client = client;
|
|
1359
203
|
this.document = new LiveEditorDocument(client);
|
|
1360
204
|
}
|
|
1361
|
-
/**
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
* Blender runs headless in the editor tab's worker (ARCHITECTURE-CORE, "THE
|
|
1365
|
-
* BLENDER IN THE TAB IS BLENDER") and answers `blender-start`,
|
|
1366
|
-
* `blender-execute`, `blender-scene-info`, `blender-object-info`,
|
|
1367
|
-
* `blender-screenshot-view`, `blender-read-file`, `blender-write-file`,
|
|
1368
|
-
* `blender-list-files`, `blender-stop` and `blender-status`. They were
|
|
1369
|
-
* reachable from `@volter/sdk` and through `volter blender-mcp` but from
|
|
1370
|
-
* no GENERAL door, so driving a session meant writing an MCP client script
|
|
1371
|
-
* per question — the same discovery failure `eval-surface.ts`'s header
|
|
1372
|
-
* records, in a lane that had not noticed it yet.
|
|
1373
|
-
*
|
|
1374
|
-
* cyclotron eval "await editor.blender('blender-execute', { code: 'import bpy; print(len(bpy.data.objects))' })"
|
|
1375
|
-
*
|
|
1376
|
-
* `blender-status` is the only verb that creates nothing: it answers whether
|
|
1377
|
-
* this tab already has a session without starting one.
|
|
1378
|
-
*/
|
|
1379
|
-
async blender(type, fields = {}) {
|
|
1380
|
-
return this.#client.blender(type, fields);
|
|
1381
|
-
}
|
|
1382
|
-
/**
|
|
1383
|
-
* MODEL PLAY'S LOG, from `cyclotron eval` — the same read as
|
|
1384
|
-
* `cyclotron play-log`. A play script writes it with
|
|
1385
|
-
* `play.log(kind, facts)`; the runner adds `play-start`, `script-reload`,
|
|
1386
|
-
* `script-error` and `play-stop`. Every entry carries `simT` (simulation
|
|
1387
|
-
* seconds) and `tick` (the frame). Each model document keeps its own: the
|
|
1388
|
-
* current run, or the last one after Stop; Play from a fresh copy empties it.
|
|
1389
|
-
* `documentId` picks one (`documents` lists them); the active Play's by default.
|
|
1390
|
-
*
|
|
1391
|
-
* cyclotron eval "(await editor.modelPlayLog({ kind: 'death' })).entries"
|
|
1392
|
-
*/
|
|
1393
|
-
async modelPlayLog(query) {
|
|
1394
|
-
return this.#client.modelPlayLog(query);
|
|
205
|
+
/** A verb a composed package contributes (`<package>-<verb>`), waited on for its own row's budget. */
|
|
206
|
+
async contributedVerb(type, fields = {}) {
|
|
207
|
+
return this.#client.contributedVerb(type, fields);
|
|
1395
208
|
}
|
|
1396
209
|
/**
|
|
1397
210
|
* The active authoring adapter's persistence destination — where a save would
|
|
1398
|
-
* land (`status().savePath`). A read only: a
|
|
211
|
+
* land (`status().savePath`). A read only: a stage root has no scene document
|
|
1399
212
|
* to open, and its root is activated instead.
|
|
1400
213
|
*/
|
|
1401
214
|
async scene() {
|
|
@@ -1414,19 +227,10 @@ var LiveEditor = class {
|
|
|
1414
227
|
async currentView() {
|
|
1415
228
|
return this.#client.currentView();
|
|
1416
229
|
}
|
|
1417
|
-
/**
|
|
1418
|
-
*
|
|
1419
|
-
*
|
|
1420
|
-
*
|
|
1421
|
-
* an unstaged look at a model. `{width, height}` asks for a shaped frame, so
|
|
1422
|
-
* a video-aspect look needs no crop afterwards. Both are bounded by the
|
|
1423
|
-
* relay budget (64-1024 per side, total no larger than a 1024 square); see
|
|
1424
|
-
* `@volter/sdk`'s `CaptureDimensions`.
|
|
1425
|
-
* Supply a view to present and photograph it in one editor request.
|
|
1426
|
-
*/
|
|
1427
|
-
/** The active document's RENDER alone, at the size asked for: what the document draws,
|
|
1428
|
-
* without what the viewport draws over it (its navigation gizmo, its readouts). To see a
|
|
1429
|
-
* stage as the person does, use `captureEditorChrome({ region: 'document' })`. */
|
|
230
|
+
/** Capture the active document's render without viewport navigation gizmos or readouts.
|
|
231
|
+
* A number requests a square; `{width, height}` requests a shaped frame, bounded by
|
|
232
|
+
* `CaptureDimensions`. Supply a view to present and capture it in one request.
|
|
233
|
+
* Use `captureEditorChrome({ region: 'document' })` to include viewport chrome. */
|
|
1430
234
|
async captureActiveDocument(size, view) {
|
|
1431
235
|
return this.#client.captureActiveDocument(size, view);
|
|
1432
236
|
}
|
|
@@ -1473,7 +277,7 @@ var LiveEditor = class {
|
|
|
1473
277
|
/**
|
|
1474
278
|
* WATCH THE AGENT LOOK AROUND THE MODEL.
|
|
1475
279
|
*
|
|
1476
|
-
* Swings the open
|
|
280
|
+
* Swings the open asset document's camera — the camera the human's tab is
|
|
1477
281
|
* showing — around the framed subject by `azimuth`/`elevation` RADIANS,
|
|
1478
282
|
* animated over `duration` seconds (default 0.6), and resolves when the move
|
|
1479
283
|
* ends. This is deliberately not a jump cut: the point of the verb is that a
|
|
@@ -1517,21 +321,6 @@ var LiveEditor = class {
|
|
|
1517
321
|
async view(preset) {
|
|
1518
322
|
await this.#client.viewPreset(preset);
|
|
1519
323
|
}
|
|
1520
|
-
/**
|
|
1521
|
-
* Switch the editor's NAMED WORKSPACE — `await editor.workspace('model')`.
|
|
1522
|
-
*
|
|
1523
|
-
* A workspace is a task-named LAYOUT MEMORY over the one dock
|
|
1524
|
-
* (ARCHITECTURE-CORE §Editor chrome): `game` (the default, the editor's
|
|
1525
|
-
* standing arrangement), `model`, `sculpt`, `texture`, `animate`, `look`.
|
|
1526
|
-
* Switching is an EXPLICIT act — nothing in the editor moves chrome on its
|
|
1527
|
-
* own, opening a document included — and this is the session door to it,
|
|
1528
|
-
* beside `Window → Workspace` and the registered actions.
|
|
1529
|
-
*
|
|
1530
|
-
* Resolves once the dock has finished rebuilding, so a capture taken
|
|
1531
|
-
* immediately after photographs the arrangement that was asked for. Each
|
|
1532
|
-
* workspace remembers the user's own hand-tuning per project, so switching
|
|
1533
|
-
* away and back is lossless.
|
|
1534
|
-
*/
|
|
1535
324
|
async workspace(id) {
|
|
1536
325
|
await this.#client.setWorkspace(id);
|
|
1537
326
|
}
|
|
@@ -1546,34 +335,18 @@ var LiveEditor = class {
|
|
|
1546
335
|
}
|
|
1547
336
|
/**
|
|
1548
337
|
* A document stage's viewport PRESENTATION — its draw mode, lighting, backdrop and overlays
|
|
1549
|
-
* (`@volter/sdk/
|
|
338
|
+
* (`@volter/sdk/viewport-presentation`) — resolved. With `layer`, that choice is
|
|
1550
339
|
* recorded for the view first, as a person's toolbar change would be, e.g.
|
|
1551
340
|
* `presentation('model:src/models/cube.blend', { all: { lighting: { studioPreset: 'kit' } } })`.
|
|
1552
|
-
* With a STRING, the named view of that id (`*.view.ts`, e.g. `'godot'`) is put on the view
|
|
1553
|
-
* whole; the answer's `viewPresets` lists the ones registered.
|
|
1554
341
|
*/
|
|
1555
342
|
async presentation(documentId, layer) {
|
|
1556
343
|
return this.#client.viewportPresentation(documentId, layer);
|
|
1557
344
|
}
|
|
1558
|
-
/**
|
|
1559
|
-
* Set the MATERIAL apart from the bundle that usually carries it.
|
|
1560
|
-
* Appearance is palette × material, independent axes by ruling, so
|
|
1561
|
-
* `style()` alone can never say whether a cost belongs to the blur or to
|
|
1562
|
-
* the palette. This is the door that measures them apart; it answers with
|
|
1563
|
-
* what the chrome wears afterwards (`style` is `null` when the mix matches
|
|
1564
|
-
* no registered bundle).
|
|
1565
|
-
*/
|
|
1566
|
-
async appearance(appearance) {
|
|
1567
|
-
return this.#client.setAppearance(appearance);
|
|
1568
|
-
}
|
|
1569
|
-
/** Focus an editor panel: a viewport tab, the console, the build surface, or
|
|
345
|
+
/** Focus an editor panel: the console, the build surface, or
|
|
1570
346
|
* any key the editor's static-panel registry holds — an unknown key refuses
|
|
1571
347
|
* naming the ones it does. */
|
|
1572
348
|
async showPanel(name) {
|
|
1573
349
|
switch (name) {
|
|
1574
|
-
case "viewport-edit":
|
|
1575
|
-
await this.#client.showViewport("edit");
|
|
1576
|
-
return;
|
|
1577
350
|
case "console":
|
|
1578
351
|
await this.#client.toggleConsole();
|
|
1579
352
|
return;
|
|
@@ -1584,21 +357,16 @@ var LiveEditor = class {
|
|
|
1584
357
|
}
|
|
1585
358
|
/** `kind` inferred from `path`'s extension when omitted (`inferAssetKind`) — pass it explicitly to override. */
|
|
1586
359
|
async openAsset(path, kind) {
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
);
|
|
360
|
+
const document = (await this.#client.documentTable()).entries.find((entry) => entry.source?.path === path);
|
|
361
|
+
if (document)
|
|
362
|
+
throw new Error(`${path} is a ${document.kind} document, not an asset: open it with editor.open("${document.id}").`);
|
|
1591
363
|
await this.#client.openAsset(path, kind ?? inferAssetKind(path));
|
|
1592
364
|
}
|
|
1593
365
|
/**
|
|
1594
366
|
* SELECT a project asset — the browser's single click, which fills the
|
|
1595
367
|
* Inspector without opening a document. `openAsset` is the double click.
|
|
1596
|
-
*
|
|
1597
|
-
*
|
|
1598
|
-
* the file it matches, then `inspect()` lists the verbs that section
|
|
1599
|
-
* declares and `runAction(id)` runs one. Selecting a path nothing matches
|
|
1600
|
-
* is not an error — the Inspector shows what it has, exactly as it does
|
|
1601
|
-
* for a human.
|
|
368
|
+
* Selecting a path nothing matches is not an error — the Inspector shows
|
|
369
|
+
* what it has, exactly as it does for a human.
|
|
1602
370
|
*/
|
|
1603
371
|
async selectAsset(path) {
|
|
1604
372
|
await this.#client.selectAsset(path);
|
|
@@ -1611,8 +379,8 @@ var LiveEditor = class {
|
|
|
1611
379
|
* where it stands under the scene's own lighting, reachable from here — or
|
|
1612
380
|
* RAW GLB BYTES (`assetPreview({ glbBase64 }, …)`), for a model that exists
|
|
1613
381
|
* only in the calling Node process's memory and has never been written to
|
|
1614
|
-
* disk. `stage` defaults to `'lab'`, the neutral Asset Lab staging
|
|
1615
|
-
*
|
|
382
|
+
* disk. `stage` defaults to `'lab'`, the neutral Asset Lab staging, and the
|
|
383
|
+
* bytes form is lab-only.
|
|
1616
384
|
*/
|
|
1617
385
|
async assetPreview(source, options) {
|
|
1618
386
|
return this.#client.captureAssetPreview(
|
|
@@ -1626,8 +394,6 @@ var LiveEditor = class {
|
|
|
1626
394
|
* crops, rendered against the asset's own skeleton, with a contact sheet.
|
|
1627
395
|
* Every source {@link assetPreview} takes works here, GLB bytes included:
|
|
1628
396
|
* a shot set stages its own subject, so it needs no place to stand.
|
|
1629
|
-
*
|
|
1630
|
-
* Sole in-repo caller today: `project.bake.preview`'s `--orbit` lane.
|
|
1631
397
|
*/
|
|
1632
398
|
async assetPreviewShots(source, definition, options) {
|
|
1633
399
|
return this.#client.captureShotSetPreview(
|
|
@@ -1648,33 +414,6 @@ var LiveEditor = class {
|
|
|
1648
414
|
async shading(mode) {
|
|
1649
415
|
await this.#client.setShadingMode(mode);
|
|
1650
416
|
}
|
|
1651
|
-
/**
|
|
1652
|
-
* READ the inspector, as data — the serialized inspection subject
|
|
1653
|
-
* (`editor.inspect()`; design: `docs/ARCHITECTURE-CORE.md` §Editor chrome,
|
|
1654
|
-
* "The Inspection Model"). This is the Figma-Inspect analog: whatever a
|
|
1655
|
-
* human would see in the inspector right now — the subject's identity, its
|
|
1656
|
-
* verbs, and every identified section in display order, with a `fields`
|
|
1657
|
-
* section's CURRENT VALUES at their scriptable `path`s.
|
|
1658
|
-
*
|
|
1659
|
-
* Reach for it whenever the next step depends on what an object actually
|
|
1660
|
-
* IS: `await editor.select(id)` then `await editor.inspect()` answers "what
|
|
1661
|
-
* properties does this thing have, and what are they set to" in one call,
|
|
1662
|
-
* against the same model the panel renders — no scene-graph reads, no
|
|
1663
|
-
* guessing at property names.
|
|
1664
|
-
*
|
|
1665
|
-
* When the inspector is showing NOTHING — nothing selected on a surface
|
|
1666
|
-
* with no empty-state subject of its own, which is most of them — the answer
|
|
1667
|
-
* is `{none: true}`, so "the human sees no inspector" and "the read failed"
|
|
1668
|
-
* are never the same value. A surface whose empty space IS a real thing (an
|
|
1669
|
-
* open Asset Lab document) still answers with that subject, and never with
|
|
1670
|
-
* another surface's.
|
|
1671
|
-
*
|
|
1672
|
-
* A `custom` section body is a named opaque: the editor renders it with
|
|
1673
|
-
* React, so the wire reports its identity rather than pretending to describe
|
|
1674
|
-
* its rendering — plus, when the section can say what it DISPLAYS, a `data`
|
|
1675
|
-
* payload in its own vocabulary (`transform` carries
|
|
1676
|
-
* `{position, rotation, scale}`, rotation in Euler XYZ degrees).
|
|
1677
|
-
*/
|
|
1678
417
|
async inspect() {
|
|
1679
418
|
return this.#client.inspect();
|
|
1680
419
|
}
|
|
@@ -1684,32 +423,11 @@ var LiveEditor = class {
|
|
|
1684
423
|
return this.#client.runInspectionAction(actionId);
|
|
1685
424
|
}
|
|
1686
425
|
/**
|
|
1687
|
-
* Run
|
|
1688
|
-
*
|
|
1689
|
-
*
|
|
1690
|
-
* onto the one view-verb table — this one and `editor.viewVerb(view, verb)`,
|
|
1691
|
-
* which addressed the same registry by its two halves. A second addressing
|
|
1692
|
-
* of one table is a second name for one thing, and an agent reading
|
|
1693
|
-
* `--list` had to choose between them with nothing to choose on. This door
|
|
1694
|
-
* stays because it is strictly wider: it addresses a COMMAND ID, so under
|
|
1695
|
-
* the frame it reaches everything the workbench knows — a `volter.action.<id>`
|
|
1696
|
-
* editor action, one of VS Code's own — and not only a view. A VIEW is
|
|
1697
|
-
* reached by spelling its verb's command id:
|
|
1698
|
-
*
|
|
1699
|
-
* await editor.command('volter.blender-uv-view.state')
|
|
1700
|
-
* await editor.command('volter.blender-uv-view.zoom', { to: 600 })
|
|
426
|
+
* Run a command by id. Under the native frame this uses ICommandService and
|
|
427
|
+
* can invoke editor or VS Code commands. Standalone sessions resolve registered
|
|
428
|
+
* view verbs of the form `volter.<view>.<verb>` and refuse other ids.
|
|
1701
429
|
*
|
|
1702
|
-
*
|
|
1703
|
-
* await editor.command('volter.blender-node-view.look', { node: 'Principled BSDF' })
|
|
1704
|
-
*
|
|
1705
|
-
* Under the Code-OSS frame this is the workbench's own command service, so
|
|
1706
|
-
* any command id works — ours and VS Code's alike. Standalone the editor's `edit` command
|
|
1707
|
-
* has no command service and answers the `volter.<view>.<verb>` shape off the
|
|
1708
|
-
* SAME verb table the frame's commands call, refusing any other id by name.
|
|
1709
|
-
* One table, two doors, exactly like the keymap's.
|
|
1710
|
-
*
|
|
1711
|
-
* Answers with whatever the command returned — a view verb's own state, or
|
|
1712
|
-
* `null` for a command that returns nothing.
|
|
430
|
+
* Returns the command’s result, or `null` when it returns nothing.
|
|
1713
431
|
*/
|
|
1714
432
|
async command(commandId, args) {
|
|
1715
433
|
return this.#client.runCommand(commandId, args);
|
|
@@ -1724,11 +442,6 @@ var LiveEditor = class {
|
|
|
1724
442
|
* helpers the menu items call, so there is one implementation of each op and
|
|
1725
443
|
* not a second that can disagree with what a human gets.
|
|
1726
444
|
*
|
|
1727
|
-
* It exists because the menu is a POINTER surface: every one of these ops was
|
|
1728
|
-
* reachable only by right-clicking a hierarchy row, which is nothing an agent
|
|
1729
|
-
* can do — so for an ingest root, whose only authoring surface IS the editor,
|
|
1730
|
-
* structure was closed entirely.
|
|
1731
|
-
*
|
|
1732
445
|
* `id`/`ids` default to the current selection. The answer carries the same
|
|
1733
446
|
* per-edit `write` ack `setField` does, so `write.persisted` tells a saved
|
|
1734
447
|
* restructure from a live-only one. An op the active adapter does not provide
|
|
@@ -1764,23 +477,14 @@ var LiveEditor = class {
|
|
|
1764
477
|
*
|
|
1765
478
|
* The companion to {@link inspect}: that one answers "what IS the selected
|
|
1766
479
|
* thing", this one answers "what does the tree LOOK LIKE". It is the panel's
|
|
1767
|
-
* own
|
|
1768
|
-
*
|
|
1769
|
-
* instanced pools), after the internals reveal, the document promotion, the
|
|
1770
|
-
* child cap, the collapse state, the search filter and the selection scope.
|
|
1771
|
-
*
|
|
1772
|
-
* Works in play mode and edit mode; the answer says which (`playState`,
|
|
1773
|
-
* `activeViewportTab`), because the two are different adapters and a tree
|
|
1774
|
-
* that looks wrong is very often the wrong adapter's tree.
|
|
480
|
+
* own projection of component marks, document promotion, child caps,
|
|
481
|
+
* collapse state, search and selection scope onto the adapter tree.
|
|
1775
482
|
*
|
|
1776
483
|
* Prefer this over `status().entities`, which is deliberately a different
|
|
1777
484
|
* question — the RAW adapter tree, unprojected. A panel that renders the
|
|
1778
485
|
* wrong rows looks perfectly healthy in that facet.
|
|
1779
486
|
*
|
|
1780
|
-
* Each row
|
|
1781
|
-
* (what is folded behind "Reveal Internals") and `expandable` (whether the
|
|
1782
|
-
* panel draws a caret at all), so "this subtree exists but nothing in the UI
|
|
1783
|
-
* opens it" is a fact you can read rather than one you have to notice.
|
|
487
|
+
* Each row reports the projected child count and whether the panel draws a disclosure control.
|
|
1784
488
|
*
|
|
1785
489
|
* Rejects, naming the panel, when no hierarchy panel is mounted — an empty
|
|
1786
490
|
* tree would be a fabricated answer about a surface nobody is being shown.
|
|
@@ -1855,36 +559,6 @@ var LiveEditor = class {
|
|
|
1855
559
|
}
|
|
1856
560
|
};
|
|
1857
561
|
|
|
1858
|
-
// src/tools.ts
|
|
1859
|
-
var LiveTools = class {
|
|
1860
|
-
/** `#`-private for the same reason `LiveEditor.#client` is. */
|
|
1861
|
-
#client;
|
|
1862
|
-
constructor(client) {
|
|
1863
|
-
this.#client = client;
|
|
1864
|
-
}
|
|
1865
|
-
/** Enumerate the exact `package.json#volter.tools` catalog without executing it. */
|
|
1866
|
-
async list() {
|
|
1867
|
-
return this.#client.listProjectTools();
|
|
1868
|
-
}
|
|
1869
|
-
/** Return one tool's discoverable metadata, or `null` when it is not registered. */
|
|
1870
|
-
async describe(name) {
|
|
1871
|
-
const catalog = await this.list();
|
|
1872
|
-
return catalog.tools.find((tool) => tool.name === name) ?? null;
|
|
1873
|
-
}
|
|
1874
|
-
/**
|
|
1875
|
-
* Invoke the same validated callable used by the editor and CLI.
|
|
1876
|
-
*
|
|
1877
|
-
* `instance` names WHICH mounted instance the tool should drive when several
|
|
1878
|
-
* are live (multiplayer authoring) — it reaches the tool as `ctx.instance`,
|
|
1879
|
-
* and a tool that drives the game binds
|
|
1880
|
-
* `game.instance(ctx.instance)` from it. Omitted is the single-instance case;
|
|
1881
|
-
* the tool then targets the sole live instance, exactly as before.
|
|
1882
|
-
*/
|
|
1883
|
-
async run(name, input = {}, options = {}) {
|
|
1884
|
-
return this.#client.runProjectTool(name, input, options);
|
|
1885
|
-
}
|
|
1886
|
-
};
|
|
1887
|
-
|
|
1888
562
|
// src/lazy-proxy.ts
|
|
1889
563
|
function isFunction(value) {
|
|
1890
564
|
return typeof value === "function";
|
|
@@ -1924,355 +598,35 @@ function lazyChainProxy(resolveRoot, path = [], hint) {
|
|
|
1924
598
|
}
|
|
1925
599
|
|
|
1926
600
|
// src/session.ts
|
|
1927
|
-
import { existsSync
|
|
1928
|
-
import { dirname
|
|
601
|
+
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
602
|
+
import { dirname, join as join2, resolve } from "node:path";
|
|
1929
603
|
|
|
1930
604
|
// ../project/src/manifest/locate.ts
|
|
1931
|
-
import { existsSync } from "node:fs";
|
|
1932
605
|
import { join } from "node:path";
|
|
1933
606
|
|
|
1934
607
|
// ../project/src/manifest/filename.ts
|
|
1935
608
|
var MANIFEST_FILENAME = "volter.project.json";
|
|
1936
|
-
var REMOVED_MANIFEST_FILENAME = "volter.game.json";
|
|
1937
|
-
function removedManifestFilenameMessage(where) {
|
|
1938
|
-
return `${where}: found \`${REMOVED_MANIFEST_FILENAME}\` and no \`${MANIFEST_FILENAME}\`. \`${REMOVED_MANIFEST_FILENAME}\` was REMOVED (the legacy-removal doctrine, docs/ARCHITECTURE-CORE.md \xA7Vocabulary) \u2014 nothing reads it any more, and it is deliberately NOT read as a fallback, because a second accepted name is the defect.
|
|
1939
|
-
Fix: rename it \u2014 \`git mv ${REMOVED_MANIFEST_FILENAME} ${MANIFEST_FILENAME}\`. The contents are unchanged; only the filename moved.`;
|
|
1940
|
-
}
|
|
1941
609
|
|
|
1942
610
|
// ../project/src/manifest/locate.ts
|
|
1943
|
-
function assertNoRemovedManifestFilename(dir) {
|
|
1944
|
-
if (existsSync(join(dir, MANIFEST_FILENAME))) return;
|
|
1945
|
-
if (!existsSync(join(dir, REMOVED_MANIFEST_FILENAME))) return;
|
|
1946
|
-
throw new Error(removedManifestFilenameMessage(join(dir, REMOVED_MANIFEST_FILENAME)));
|
|
1947
|
-
}
|
|
1948
611
|
function resolveManifestPath(dir) {
|
|
1949
|
-
assertNoRemovedManifestFilename(dir);
|
|
1950
612
|
return join(dir, MANIFEST_FILENAME);
|
|
1951
613
|
}
|
|
1952
614
|
|
|
1953
|
-
// ../sdk/src/session/product-locator.ts
|
|
1954
|
-
import { existsSync as existsSync2, readFileSync } from "node:fs";
|
|
1955
|
-
import { createRequire } from "node:module";
|
|
1956
|
-
import { dirname, join as join2 } from "node:path";
|
|
1957
|
-
import { pathToFileURL } from "node:url";
|
|
1958
|
-
var PRODUCT_DECLARATION_KEY = "volter.product";
|
|
1959
|
-
var PRODUCT_INSTALL_LINES = [
|
|
1960
|
-
" npm install --save-dev @volter/cyclotron",
|
|
1961
|
-
" npm install --save-dev @volter/game-editor"
|
|
1962
|
-
];
|
|
1963
|
-
var PRODUCT_CREATE_LINES = [" npx @volter/editor-core create <name>"];
|
|
1964
|
-
var PRODUCT_COLOR_THEMES = ["dark", "light"];
|
|
1965
|
-
function readProductManifest(packageDir) {
|
|
1966
|
-
const manifestPath = join2(packageDir, "package.json");
|
|
1967
|
-
let manifest;
|
|
1968
|
-
try {
|
|
1969
|
-
manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
1970
|
-
} catch {
|
|
1971
|
-
return null;
|
|
1972
|
-
}
|
|
1973
|
-
const declared = manifest.volter?.product;
|
|
1974
|
-
if (declared === void 0) return null;
|
|
1975
|
-
const entry = declared.entry;
|
|
1976
|
-
if (typeof entry !== "string" || !entry.startsWith("./"))
|
|
1977
|
-
throw new Error(
|
|
1978
|
-
`${manifestPath} declares ${PRODUCT_DECLARATION_KEY} but its "entry" is ${JSON.stringify(entry)}. It must be a ./-relative module \u2014 the product's ONE source entry, which is also its frame entry (e.g. "./src/index.ts").`
|
|
1979
|
-
);
|
|
1980
|
-
const resolved = join2(packageDir, entry);
|
|
1981
|
-
if (!existsSync2(resolved))
|
|
1982
|
-
throw new Error(
|
|
1983
|
-
`${manifestPath} declares ${PRODUCT_DECLARATION_KEY}.entry ${JSON.stringify(entry)}, which does not exist (${resolved}).`
|
|
1984
|
-
);
|
|
1985
|
-
const colorTheme = declared.colorTheme;
|
|
1986
|
-
if (typeof colorTheme !== "string" || !PRODUCT_COLOR_THEMES.includes(colorTheme))
|
|
1987
|
-
throw new Error(
|
|
1988
|
-
`${manifestPath} declares ${PRODUCT_DECLARATION_KEY} but its "colorTheme" is ${JSON.stringify(colorTheme)}. It must be ${PRODUCT_COLOR_THEMES.map(
|
|
1989
|
-
(one) => JSON.stringify(one)
|
|
1990
|
-
).join(" or ")} \u2014 which way round this product paints, read before any of its code runs so the workbench's first frame is already the product's own background.`
|
|
1991
|
-
);
|
|
1992
|
-
const bin = manifest.bin;
|
|
1993
|
-
const commands = bin !== null && typeof bin === "object" ? Object.keys(bin) : [];
|
|
1994
|
-
if (commands.length !== 1)
|
|
1995
|
-
throw new Error(
|
|
1996
|
-
`${manifestPath} declares ${PRODUCT_DECLARATION_KEY} but its "bin" names ${commands.length === 0 ? "no command" : `${commands.length} commands`}. A product has exactly one command \u2014 the one every session message tells a person to run.`
|
|
1997
|
-
);
|
|
1998
|
-
const displayName = declared.displayName;
|
|
1999
|
-
if (typeof displayName !== "string" || displayName.trim() === "")
|
|
2000
|
-
throw new Error(
|
|
2001
|
-
`${manifestPath} declares ${PRODUCT_DECLARATION_KEY} but its "displayName" is ${JSON.stringify(displayName)}. It must be the name a person sees (e.g. "Volter Editor").`
|
|
2002
|
-
);
|
|
2003
|
-
const name = typeof manifest.name === "string" ? manifest.name : packageDir;
|
|
2004
|
-
const install = declared.install;
|
|
2005
|
-
if (install !== void 0 && (typeof install !== "string" || install.trim() === ""))
|
|
2006
|
-
throw new Error(
|
|
2007
|
-
`${manifestPath} declares ${PRODUCT_DECLARATION_KEY} but its "install" is ${JSON.stringify(install)}. It is the one line that installs this product and starts a project (e.g. "npx ${name} create my-game"), or absent.`
|
|
2008
|
-
);
|
|
2009
|
-
return {
|
|
2010
|
-
name,
|
|
2011
|
-
version: typeof manifest.version === "string" ? manifest.version : "unknown",
|
|
2012
|
-
command: commands[0],
|
|
2013
|
-
displayName,
|
|
2014
|
-
upgrade: declared.upgrade === true,
|
|
2015
|
-
adapterRequired: declared.adapterRequired === true,
|
|
2016
|
-
dir: packageDir,
|
|
2017
|
-
entry,
|
|
2018
|
-
colorTheme,
|
|
2019
|
-
// A private source product may use an explicitly selected matching checkout.
|
|
2020
|
-
// Public packages must always carry a downloadable, checksummed workbench.
|
|
2021
|
-
workbench: manifest.private === true && declared.workbench === void 0 ? null : readProductWorkbench(manifestPath, declared.workbench),
|
|
2022
|
-
install: typeof install === "string" ? install.trim() : `npx ${name} create my-game`
|
|
2023
|
-
};
|
|
2024
|
-
}
|
|
2025
|
-
var SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
2026
|
-
function readProductWorkbench(manifestPath, declared) {
|
|
2027
|
-
const example = ' "volter": { "product": { "workbench": { "release": "<product>-<fork sha 12>-<platform>", "tarballSha256": "<64 hex>" } } }\n`node scripts/workbench/build-release.mjs --publish --out <release dir>` cuts the release and prints exactly that block.';
|
|
2028
|
-
if (typeof declared !== "object" || declared === null || Array.isArray(declared))
|
|
2029
|
-
throw new Error(
|
|
2030
|
-
`${manifestPath} declares ${PRODUCT_DECLARATION_KEY} but its "workbench" is ${JSON.stringify(declared)}. A product IS the Code-OSS workbench it was compiled into, so it names the published release those bytes are:
|
|
2031
|
-
${example}`
|
|
2032
|
-
);
|
|
2033
|
-
const platforms = declared;
|
|
2034
|
-
if (!("release" in platforms) && !("tarballSha256" in platforms)) {
|
|
2035
|
-
const machine = `${process.platform}-${process.arch}`;
|
|
2036
|
-
const declaredHere = platforms[machine];
|
|
2037
|
-
if (declaredHere === void 0)
|
|
2038
|
-
throw new Error(
|
|
2039
|
-
`${manifestPath}'s ${PRODUCT_DECLARATION_KEY}.workbench declares releases for ${Object.keys(platforms).join(", ") || "no platform"} and this machine is ${machine}. A Code-OSS server package carries its own platform's node binary and native modules, so there is nothing this product can run in here until a ${machine} release is cut:
|
|
2040
|
-
node scripts/workbench/build-release.mjs --product <id> --platform ${machine} --checkout <fork dir> --publish`
|
|
2041
|
-
);
|
|
2042
|
-
return readPlatformWorkbench(manifestPath, declaredHere, example);
|
|
2043
|
-
}
|
|
2044
|
-
return readPlatformWorkbench(manifestPath, declared, example);
|
|
2045
|
-
}
|
|
2046
|
-
function readPlatformWorkbench(manifestPath, declared, example) {
|
|
2047
|
-
if (typeof declared !== "object" || declared === null || Array.isArray(declared))
|
|
2048
|
-
throw new Error(
|
|
2049
|
-
`${manifestPath}'s ${PRODUCT_DECLARATION_KEY}.workbench names ${JSON.stringify(declared)} for this platform, which is not a release:
|
|
2050
|
-
${example}`
|
|
2051
|
-
);
|
|
2052
|
-
const record = declared;
|
|
2053
|
-
const extra = Object.keys(record).filter((key) => key !== "release" && key !== "tarballSha256");
|
|
2054
|
-
if (extra.length > 0)
|
|
2055
|
-
throw new Error(
|
|
2056
|
-
`${manifestPath}'s ${PRODUCT_DECLARATION_KEY}.workbench carries ${extra.map((key) => `"${key}"`).join(", ")}, which this declaration has no meaning for. It names the release and the pin, and nothing else:
|
|
2057
|
-
${example}`
|
|
2058
|
-
);
|
|
2059
|
-
const release = record["release"];
|
|
2060
|
-
if (typeof release !== "string" || release.trim() === "")
|
|
2061
|
-
throw new Error(
|
|
2062
|
-
`${manifestPath}'s ${PRODUCT_DECLARATION_KEY}.workbench names no "release" \u2014 the GitHub Release tag its workbench was published under:
|
|
2063
|
-
${example}`
|
|
2064
|
-
);
|
|
2065
|
-
const tarballSha256 = record["tarballSha256"];
|
|
2066
|
-
if (typeof tarballSha256 !== "string" || !SHA256_HEX.test(tarballSha256))
|
|
2067
|
-
throw new Error(
|
|
2068
|
-
`${manifestPath}'s ${PRODUCT_DECLARATION_KEY}.workbench has tarballSha256 ${JSON.stringify(tarballSha256)}, which is not 64 hex characters. It is the PIN \u2014 the hash the downloaded tarball is checked against before a byte of it is extracted, so it is never approximate:
|
|
2069
|
-
${example}`
|
|
2070
|
-
);
|
|
2071
|
-
return { release, tarballSha256 };
|
|
2072
|
-
}
|
|
2073
|
-
function declaredVolterPackages(projectRoot) {
|
|
2074
|
-
let manifest;
|
|
2075
|
-
try {
|
|
2076
|
-
manifest = JSON.parse(
|
|
2077
|
-
readFileSync(join2(projectRoot, "package.json"), "utf8")
|
|
2078
|
-
);
|
|
2079
|
-
} catch {
|
|
2080
|
-
return [];
|
|
2081
|
-
}
|
|
2082
|
-
return Object.keys({ ...manifest.dependencies ?? {}, ...manifest.devDependencies ?? {} }).filter((name) => name.startsWith("@volter/")).sort();
|
|
2083
|
-
}
|
|
2084
|
-
function locatePackage(projectRoot, name) {
|
|
2085
|
-
const require2 = createRequire(pathToFileURL(join2(projectRoot, "package.json")));
|
|
2086
|
-
try {
|
|
2087
|
-
return dirname(require2.resolve(`${name}/package.json`));
|
|
2088
|
-
} catch {
|
|
2089
|
-
try {
|
|
2090
|
-
let dir = dirname(require2.resolve(name));
|
|
2091
|
-
for (; ; ) {
|
|
2092
|
-
const candidate = join2(dir, "package.json");
|
|
2093
|
-
try {
|
|
2094
|
-
if (JSON.parse(readFileSync(candidate, "utf8")).name === name)
|
|
2095
|
-
return dir;
|
|
2096
|
-
} catch {
|
|
2097
|
-
}
|
|
2098
|
-
const parent = dirname(dir);
|
|
2099
|
-
if (parent === dir) return null;
|
|
2100
|
-
dir = parent;
|
|
2101
|
-
}
|
|
2102
|
-
} catch {
|
|
2103
|
-
return null;
|
|
2104
|
-
}
|
|
2105
|
-
}
|
|
2106
|
-
}
|
|
2107
|
-
function declaredProducts(projectRoot) {
|
|
2108
|
-
const found = [];
|
|
2109
|
-
for (const name of declaredVolterPackages(projectRoot)) {
|
|
2110
|
-
const dir = locatePackage(projectRoot, name);
|
|
2111
|
-
if (dir === null) continue;
|
|
2112
|
-
const product = readProductManifest(dir);
|
|
2113
|
-
if (product !== null) found.push(product);
|
|
2114
|
-
}
|
|
2115
|
-
return found;
|
|
2116
|
-
}
|
|
2117
|
-
function resolveProductForProject(projectRoot) {
|
|
2118
|
-
const products = declaredProducts(projectRoot);
|
|
2119
|
-
if (products.length === 1) return products[0];
|
|
2120
|
-
if (products.length === 0)
|
|
2121
|
-
throw new Error(
|
|
2122
|
-
`${projectRoot} declares no editor product, so there is no editor to open it in.
|
|
2123
|
-
The installed product supplies the editor,
|
|
2124
|
-
and which one runs is what the project installed, never a flag.
|
|
2125
|
-
For THIS folder, install the one you are building with:
|
|
2126
|
-
${PRODUCT_INSTALL_LINES.join("\n")}
|
|
2127
|
-
then run that product's \`edit\` command again. For a NEW project, the product creates it:
|
|
2128
|
-
${PRODUCT_CREATE_LINES.join("\n")}`
|
|
2129
|
-
);
|
|
2130
|
-
throw new Error(
|
|
2131
|
-
`${projectRoot} declares ${products.length} editor products \u2014 ${products.map((product) => product.name).join(" and ")}.
|
|
2132
|
-
One page runs one product, and nothing here may choose between them. Remove the one you
|
|
2133
|
-
are not building with:
|
|
2134
|
-
` + products.map((product) => ` npm uninstall ${product.name}`).join("\n")
|
|
2135
|
-
);
|
|
2136
|
-
}
|
|
2137
|
-
|
|
2138
|
-
// ../sdk/src/session/registry-format.ts
|
|
2139
|
-
import {
|
|
2140
|
-
mkdirSync,
|
|
2141
|
-
readdirSync,
|
|
2142
|
-
readFileSync as readFileSync2,
|
|
2143
|
-
realpathSync,
|
|
2144
|
-
unlinkSync,
|
|
2145
|
-
writeFileSync
|
|
2146
|
-
} from "node:fs";
|
|
2147
|
-
import { homedir } from "node:os";
|
|
2148
|
-
import { join as join3, resolve } from "node:path";
|
|
2149
|
-
var EDITOR_SESSIONS_REGISTRY_FILE = join3(homedir(), ".volter", "editor-sessions.json");
|
|
2150
|
-
function isEditorSessionEntry(v) {
|
|
2151
|
-
if (typeof v !== "object" || v === null) return false;
|
|
2152
|
-
const s = v;
|
|
2153
|
-
const optionalIdentity = (value) => value === void 0 || value === null || typeof value === "string";
|
|
2154
|
-
return (typeof s["project"] === "string" || s["project"] === null) && typeof s["port"] === "number" && typeof s["pid"] === "number" && typeof s["startedAt"] === "string" && optionalIdentity(s["sessionId"]) && optionalIdentity(s["controlSecret"]) && optionalIdentity(s["repositoryId"]) && optionalIdentity(s["worktreeId"]) && optionalIdentity(s["worktreeRoot"]) && optionalIdentity(s["projectRelativePath"]) && optionalIdentity(s["branch"]) && optionalIdentity(s["headCommit"]) && optionalIdentity(s["baseCommit"]);
|
|
2155
|
-
}
|
|
2156
|
-
function normalizeEditorSessionEntry(session) {
|
|
2157
|
-
return {
|
|
2158
|
-
...session,
|
|
2159
|
-
sessionId: session.sessionId ?? null,
|
|
2160
|
-
controlSecret: session.controlSecret ?? null,
|
|
2161
|
-
repositoryId: session.repositoryId ?? null,
|
|
2162
|
-
worktreeId: session.worktreeId ?? null,
|
|
2163
|
-
worktreeRoot: session.worktreeRoot ?? null,
|
|
2164
|
-
projectRelativePath: session.projectRelativePath ?? null,
|
|
2165
|
-
branch: session.branch ?? null,
|
|
2166
|
-
headCommit: session.headCommit ?? null,
|
|
2167
|
-
baseCommit: session.baseCommit ?? null
|
|
2168
|
-
};
|
|
2169
|
-
}
|
|
2170
|
-
function pidAlive(pid) {
|
|
2171
|
-
try {
|
|
2172
|
-
process.kill(pid, 0);
|
|
2173
|
-
return true;
|
|
2174
|
-
} catch (error) {
|
|
2175
|
-
return error.code === "EPERM";
|
|
2176
|
-
}
|
|
2177
|
-
}
|
|
2178
|
-
function readLiveRegisteredSessions() {
|
|
2179
|
-
try {
|
|
2180
|
-
const raw = JSON.parse(readFileSync2(EDITOR_SESSIONS_REGISTRY_FILE, "utf8"));
|
|
2181
|
-
return Array.isArray(raw) ? raw.filter(isEditorSessionEntry).map(normalizeEditorSessionEntry).filter((s) => pidAlive(s.pid)) : [];
|
|
2182
|
-
} catch {
|
|
2183
|
-
return [];
|
|
2184
|
-
}
|
|
2185
|
-
}
|
|
2186
|
-
function servedProjectAnswer(body) {
|
|
2187
|
-
const b = body;
|
|
2188
|
-
return {
|
|
2189
|
-
path: b.project?.path ?? b.serving?.path ?? null,
|
|
2190
|
-
manifestError: b.project ? null : b.serving?.error ?? null
|
|
2191
|
-
};
|
|
2192
|
-
}
|
|
2193
|
-
|
|
2194
|
-
// ../sdk/src/session/discovery.ts
|
|
2195
|
-
var EDITOR_SESSION_DISCOVERY_TIMEOUT_MS = 2e3;
|
|
2196
|
-
var EDITOR_PROBE_TIMEOUT_MS = 1500;
|
|
2197
|
-
var EditorTimeoutError = class extends Error {
|
|
2198
|
-
constructor(label, ms) {
|
|
2199
|
-
super(`${label} timed out after ${ms}ms`);
|
|
2200
|
-
this.name = "EditorTimeoutError";
|
|
2201
|
-
}
|
|
2202
|
-
};
|
|
2203
|
-
function withTimeout(promise, ms, label) {
|
|
2204
|
-
return new Promise((resolvePromise, reject) => {
|
|
2205
|
-
const timer = setTimeout(() => reject(new EditorTimeoutError(label, ms)), ms);
|
|
2206
|
-
promise.then(
|
|
2207
|
-
(v) => {
|
|
2208
|
-
clearTimeout(timer);
|
|
2209
|
-
resolvePromise(v);
|
|
2210
|
-
},
|
|
2211
|
-
(err) => {
|
|
2212
|
-
clearTimeout(timer);
|
|
2213
|
-
reject(err instanceof Error ? err : new Error(String(err)));
|
|
2214
|
-
}
|
|
2215
|
-
);
|
|
2216
|
-
});
|
|
2217
|
-
}
|
|
2218
|
-
async function fetchJson(url, timeoutMs) {
|
|
2219
|
-
const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
2220
|
-
if (!res.ok) throw new Error(`GET /__editor/project returned HTTP ${res.status}`);
|
|
2221
|
-
return await res.json();
|
|
2222
|
-
}
|
|
2223
|
-
function baseUrl(session) {
|
|
2224
|
-
return session.url ?? `http://127.0.0.1:${session.port}`;
|
|
2225
|
-
}
|
|
2226
|
-
var HttpSessionDiscovery = class {
|
|
2227
|
-
async listSessions(timeoutMs) {
|
|
2228
|
-
const registered = readLiveRegisteredSessions();
|
|
2229
|
-
const perProbeTimeout = Math.min(EDITOR_PROBE_TIMEOUT_MS, Math.max(200, timeoutMs));
|
|
2230
|
-
const failures = [];
|
|
2231
|
-
const probes = await Promise.all(
|
|
2232
|
-
registered.map(async (s) => {
|
|
2233
|
-
try {
|
|
2234
|
-
const body = await fetchJson(
|
|
2235
|
-
`${baseUrl({ port: s.port, project: null, pid: s.pid })}/__editor/project`,
|
|
2236
|
-
perProbeTimeout
|
|
2237
|
-
);
|
|
2238
|
-
const served = servedProjectAnswer(body);
|
|
2239
|
-
const info = {
|
|
2240
|
-
port: s.port,
|
|
2241
|
-
project: served.path,
|
|
2242
|
-
pid: s.pid,
|
|
2243
|
-
manifestError: served.manifestError
|
|
2244
|
-
};
|
|
2245
|
-
return info;
|
|
2246
|
-
} catch (error) {
|
|
2247
|
-
const detail = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
2248
|
-
failures.push(`port ${s.port}: ${detail}`);
|
|
2249
|
-
return void 0;
|
|
2250
|
-
}
|
|
2251
|
-
})
|
|
2252
|
-
);
|
|
2253
|
-
const sessions = probes.filter((p) => p !== void 0);
|
|
2254
|
-
if (registered.length > 0 && sessions.length === 0) {
|
|
2255
|
-
throw new Error(`Registered editor sessions could not be inspected: ${failures.join("; ")}`);
|
|
2256
|
-
}
|
|
2257
|
-
return sessions;
|
|
2258
|
-
}
|
|
2259
|
-
};
|
|
2260
|
-
|
|
2261
615
|
// src/session.ts
|
|
2262
616
|
function findProjectRootFrom(dir) {
|
|
2263
|
-
let cur =
|
|
617
|
+
let cur = resolve(dir);
|
|
2264
618
|
for (; ; ) {
|
|
2265
|
-
if (
|
|
2266
|
-
const parent =
|
|
619
|
+
if (existsSync(resolveManifestPath(cur))) return cur;
|
|
620
|
+
const parent = dirname(cur);
|
|
2267
621
|
if (parent === cur) return null;
|
|
2268
622
|
cur = parent;
|
|
2269
623
|
}
|
|
2270
624
|
}
|
|
2271
625
|
function canonicalPath(path) {
|
|
2272
626
|
try {
|
|
2273
|
-
return
|
|
627
|
+
return realpathSync(path);
|
|
2274
628
|
} catch {
|
|
2275
|
-
return
|
|
629
|
+
return resolve(path);
|
|
2276
630
|
}
|
|
2277
631
|
}
|
|
2278
632
|
function sameDirectory(a, b) {
|
|
@@ -2285,10 +639,11 @@ function sameDirectory(a, b) {
|
|
|
2285
639
|
}
|
|
2286
640
|
}
|
|
2287
641
|
var NO_SESSION_FOR_PROJECT = "VOLTER_NO_SESSION_FOR_PROJECT";
|
|
642
|
+
var SESSION_MANIFEST_UNREADABLE = "VOLTER_SESSION_MANIFEST_UNREADABLE";
|
|
2288
643
|
function readProjectSession(projectRoot) {
|
|
2289
644
|
try {
|
|
2290
645
|
const value = JSON.parse(
|
|
2291
|
-
|
|
646
|
+
readFileSync(join2(projectRoot, ".volter", "session.json"), "utf8")
|
|
2292
647
|
);
|
|
2293
648
|
if (typeof value !== "object" || value === null) return null;
|
|
2294
649
|
const hint = value;
|
|
@@ -2311,16 +666,20 @@ async function probeServedProject(port) {
|
|
|
2311
666
|
return void 0;
|
|
2312
667
|
}
|
|
2313
668
|
}
|
|
2314
|
-
function manifestRefusal(projectRoot, manifestError) {
|
|
2315
|
-
return
|
|
2316
|
-
|
|
669
|
+
function manifestRefusal(projectRoot, manifestError, port) {
|
|
670
|
+
return Object.assign(
|
|
671
|
+
new Error(
|
|
672
|
+
`@volter/live: the editor session for ${projectRoot} is live, but its volter.project.json does not load, so there is no editor or project to drive \u2014 the editor page is showing this same error. Fix the manifest and retry; the session recovers on save, no restart needed.
|
|
2317
673
|
${manifestError}`
|
|
674
|
+
),
|
|
675
|
+
// The session is still this project's, so a caller that only needs to END it (`close`) can.
|
|
676
|
+
{ code: SESSION_MANIFEST_UNREADABLE, session: { port, projectRoot } }
|
|
2318
677
|
);
|
|
2319
678
|
}
|
|
2320
679
|
function noMatchingSessionRefusal(projectRoot, canon, sessions, discoveryFailure) {
|
|
2321
680
|
if (discoveryFailure !== null) {
|
|
2322
681
|
return new Error(
|
|
2323
|
-
`@volter/live: could not complete editor session discovery while looking for ${projectRoot} \u2014 ${discoveryFailure}. This is not the answer "no editor is running": the question went unanswered, so nothing is known about what is live.
|
|
682
|
+
`@volter/live: could not complete editor session discovery while looking for ${projectRoot} \u2014 ${discoveryFailure}. This is not the answer "no editor is running": the question went unanswered, so nothing is known about what is live. The step named above is the one that did not answer: that session's server holds its port but did not reply to the request. Its own log (logs/editor-<port>.log in the project it serves) says how far its start got; ${sessionsCommandFor(projectRoot)} asks the same question directly.`
|
|
2324
683
|
);
|
|
2325
684
|
}
|
|
2326
685
|
if (sessions.length === 0) {
|
|
@@ -2356,7 +715,7 @@ async function resolveSession(projectDir = process.cwd(), deps = {}) {
|
|
|
2356
715
|
} else {
|
|
2357
716
|
const served = await probeServedProject(localHint.port);
|
|
2358
717
|
if (served?.path != null && canonicalPath(served.path) === canonicalPath(projectRoot)) {
|
|
2359
|
-
if (served.manifestError !== null) throw manifestRefusal(projectRoot, served.manifestError);
|
|
718
|
+
if (served.manifestError !== null) throw manifestRefusal(projectRoot, served.manifestError, localHint.port);
|
|
2360
719
|
return { port: localHint.port, projectRoot };
|
|
2361
720
|
}
|
|
2362
721
|
}
|
|
@@ -2376,7 +735,7 @@ async function resolveSession(projectDir = process.cwd(), deps = {}) {
|
|
|
2376
735
|
const canon = canonicalPath(projectRoot);
|
|
2377
736
|
const match = sessions.find((s) => s.project !== null && canonicalPath(s.project) === canon);
|
|
2378
737
|
if (!match) throw noMatchingSessionRefusal(projectRoot, canon, sessions, discoveryFailure);
|
|
2379
|
-
if (match.manifestError != null) throw manifestRefusal(projectRoot, match.manifestError);
|
|
738
|
+
if (match.manifestError != null) throw manifestRefusal(projectRoot, match.manifestError, match.port);
|
|
2380
739
|
return { port: match.port, projectRoot };
|
|
2381
740
|
}
|
|
2382
741
|
function editCommandFor(projectRoot) {
|
|
@@ -2400,15 +759,12 @@ function createLazySession(factory) {
|
|
|
2400
759
|
ensure() {
|
|
2401
760
|
promise ??= factory();
|
|
2402
761
|
return promise;
|
|
2403
|
-
},
|
|
2404
|
-
reset() {
|
|
2405
|
-
promise = null;
|
|
2406
762
|
}
|
|
2407
763
|
};
|
|
2408
764
|
}
|
|
2409
765
|
|
|
2410
766
|
// src/member-hints.ts
|
|
2411
|
-
var CAMERA_HINT = "The viewport camera is posed through a presented view: await editor.present({ version: 1, viewport: { camera: { position: { x, y, z }, target: { x, y, z }, fov } } }) (stage space: metres, Y up
|
|
767
|
+
var CAMERA_HINT = "The viewport camera is posed through a presented view: await editor.present({ version: 1, viewport: { camera: { position: { x, y, z }, target: { x, y, z }, fov } } }) (stage space: metres, Y up), and read back with (await editor.currentView()).viewport.camera. editor.frame() and editor.orbit() frame or swing around the subject instead.";
|
|
2412
768
|
var EDITOR_MEMBER_HINTS = {
|
|
2413
769
|
camera: CAMERA_HINT,
|
|
2414
770
|
setCamera: CAMERA_HINT,
|
|
@@ -2422,12 +778,12 @@ var EDITOR_MEMBER_HINTS = {
|
|
|
2422
778
|
function editorMemberHint(member) {
|
|
2423
779
|
return typeof member === "string" && Object.hasOwn(EDITOR_MEMBER_HINTS, member) ? EDITOR_MEMBER_HINTS[member] : void 0;
|
|
2424
780
|
}
|
|
2425
|
-
function withEditorMemberHints(target
|
|
781
|
+
function withEditorMemberHints(target) {
|
|
2426
782
|
return new Proxy(target, {
|
|
2427
783
|
get(object, key) {
|
|
2428
784
|
if (!(key in object)) {
|
|
2429
785
|
const hint = editorMemberHint(key);
|
|
2430
|
-
if (hint !== void 0) throw new TypeError(
|
|
786
|
+
if (hint !== void 0) throw new TypeError(`editor.${String(key)} does not exist. ${hint}`);
|
|
2431
787
|
}
|
|
2432
788
|
const value = Reflect.get(object, key, object);
|
|
2433
789
|
return typeof value === "function" ? value.bind(object) : value;
|
|
@@ -2438,7 +794,7 @@ function withEditorMemberHints(target, binding = "editor") {
|
|
|
2438
794
|
// src/index.ts
|
|
2439
795
|
function bindTo(port) {
|
|
2440
796
|
const client = new EditorClient({ url: `http://127.0.0.1:${port}` });
|
|
2441
|
-
return { editor: new LiveEditor(client)
|
|
797
|
+
return { editor: new LiveEditor(client) };
|
|
2442
798
|
}
|
|
2443
799
|
function unconnectedBindings() {
|
|
2444
800
|
return bindTo(0);
|
|
@@ -2449,19 +805,17 @@ async function connect(projectDir, deps) {
|
|
|
2449
805
|
}
|
|
2450
806
|
var lazySession = createLazySession(() => connect());
|
|
2451
807
|
var editor = lazyChainProxy(() => lazySession.ensure().then((s) => s.editor), [], editorMemberHint);
|
|
2452
|
-
var tools = lazyChainProxy(() => lazySession.ensure().then((s) => s.tools));
|
|
2453
808
|
export {
|
|
2454
809
|
LiveEditor,
|
|
2455
810
|
LiveEditorDocument,
|
|
2456
|
-
LiveTools,
|
|
2457
811
|
NO_SESSION_FOR_PROJECT,
|
|
812
|
+
SESSION_MANIFEST_UNREADABLE,
|
|
2458
813
|
connect,
|
|
2459
814
|
editor,
|
|
2460
815
|
editorMemberHint,
|
|
2461
816
|
findProjectRootFrom,
|
|
2462
817
|
inferAssetKind,
|
|
2463
818
|
resolveSession,
|
|
2464
|
-
tools,
|
|
2465
819
|
unconnectedBindings,
|
|
2466
820
|
withEditorMemberHints
|
|
2467
821
|
};
|