@waica/cli 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +42 -37
  2. package/dist/editor/assets/{cssMode-2_8FrcPt.js → cssMode-DfLN2qUF.js} +1 -1
  3. package/dist/editor/assets/{freemarker2-BgSLfjNS.js → freemarker2-BM-wWcXR.js} +1 -1
  4. package/dist/editor/assets/{handlebars-CZIkUhwZ.js → handlebars-C68SV5M9.js} +1 -1
  5. package/dist/editor/assets/{html-B66ooLhD.js → html-dQgLbIki.js} +1 -1
  6. package/dist/editor/assets/{htmlMode-BiIT--Mv.js → htmlMode-Dx91fUdd.js} +1 -1
  7. package/dist/editor/assets/{index-Br-M9Tdo.js → index-592ABIaz.js} +2787 -406
  8. package/dist/editor/assets/{index-q7IkLe-P.css → index-Cy5pmBhN.css} +1 -1
  9. package/dist/editor/assets/{javascript-BZrbbzWQ.js → javascript-BmsSq5ka.js} +1 -1
  10. package/dist/editor/assets/{jsonMode-CABkbZgt.js → jsonMode-CWoPtX2M.js} +1 -1
  11. package/dist/editor/assets/{liquid-CT7pyNYe.js → liquid-DwnJlNJZ.js} +1 -1
  12. package/dist/editor/assets/{mdx-DW7MPqdm.js → mdx-DaLkvlR1.js} +1 -1
  13. package/dist/editor/assets/{python-CgBwEaNf.js → python-Cs5X5zFp.js} +1 -1
  14. package/dist/editor/assets/{razor-Dvggr_Q1.js → razor-PTwWoRk8.js} +1 -1
  15. package/dist/editor/assets/{tsMode-OfEpAJE7.js → tsMode-BqUQdgsR.js} +1 -1
  16. package/dist/editor/assets/{typescript-BQbhlbBK.js → typescript-BsJ7r2RJ.js} +1 -1
  17. package/dist/editor/assets/{xml-5qToUR1k.js → xml-DyyoPey4.js} +1 -1
  18. package/dist/editor/assets/{yaml-CFM1jPNu.js → yaml-Cjqtlp29.js} +1 -1
  19. package/dist/editor/index.html +2 -2
  20. package/dist/mcp/introspection.js +2 -0
  21. package/dist/mcp/node_modules/@waica/archetype-platformer/package.json +3 -3
  22. package/dist/mcp/node_modules/@waica/behaviors/dist/health.d.ts +4 -3
  23. package/dist/mcp/node_modules/@waica/behaviors/dist/health.js +12 -15
  24. package/dist/mcp/node_modules/@waica/behaviors/dist/out-of-bounds.d.ts +1 -0
  25. package/dist/mcp/node_modules/@waica/behaviors/dist/out-of-bounds.js +1 -0
  26. package/dist/mcp/node_modules/@waica/behaviors/package.json +2 -2
  27. package/dist/mcp/node_modules/@waica/engine/README.md +85 -0
  28. package/dist/mcp/node_modules/@waica/engine/dist/component-update-schedule.d.ts +39 -0
  29. package/dist/mcp/node_modules/@waica/engine/dist/component-update-schedule.js +157 -0
  30. package/dist/mcp/node_modules/@waica/engine/dist/component.d.ts +5 -0
  31. package/dist/mcp/node_modules/@waica/engine/dist/component.js +1 -0
  32. package/dist/mcp/node_modules/@waica/engine/dist/components/animated-sprite.d.ts +1 -0
  33. package/dist/mcp/node_modules/@waica/engine/dist/components/animated-sprite.js +1 -0
  34. package/dist/mcp/node_modules/@waica/engine/dist/game.d.ts +7 -0
  35. package/dist/mcp/node_modules/@waica/engine/dist/game.js +79 -1
  36. package/dist/mcp/node_modules/@waica/engine/dist/index.d.ts +7 -1
  37. package/dist/mcp/node_modules/@waica/engine/dist/index.js +3 -0
  38. package/dist/mcp/node_modules/@waica/engine/dist/input.d.ts +10 -0
  39. package/dist/mcp/node_modules/@waica/engine/dist/input.js +40 -2
  40. package/dist/mcp/node_modules/@waica/engine/dist/runtime-bridge.d.ts +69 -0
  41. package/dist/mcp/node_modules/@waica/engine/dist/runtime-bridge.js +105 -0
  42. package/dist/mcp/node_modules/@waica/engine/dist/runtime-inspection.d.ts +71 -0
  43. package/dist/mcp/node_modules/@waica/engine/dist/runtime-inspection.js +279 -0
  44. package/dist/mcp/node_modules/@waica/engine/package.json +3 -2
  45. package/dist/mcp/project-component-loader.js +506 -240
  46. package/dist/mcp/project-component-runner.js +237 -0
  47. package/dist/mcp/runtime-browser.js +336 -0
  48. package/dist/mcp/runtime-dev-server.js +273 -0
  49. package/dist/mcp/runtime-preflight.js +249 -0
  50. package/dist/mcp/runtime-service.js +8 -0
  51. package/dist/mcp/runtime-session-manager.js +265 -0
  52. package/dist/mcp/scaffolds.js +126 -6
  53. package/dist/mcp/server.js +330 -9
  54. package/dist/mcp/stdio.js +0 -25
  55. package/dist/mcp/template/src/main.ts +6 -1
  56. package/dist/mcp/validation.js +77 -6
  57. package/package.json +2 -1
  58. package/dist/mcp/native-import.cjs +0 -3
@@ -0,0 +1,237 @@
1
+ import { existsSync, realpathSync } from 'node:fs';
2
+ import * as nodeModule from 'node:module';
3
+ import path from 'node:path';
4
+ import { fileURLToPath, pathToFileURL } from 'node:url';
5
+ const PROTOCOL_VERSION = 1;
6
+ const { createRequire } = nodeModule;
7
+ const hostRequire = createRequire(import.meta.url);
8
+ const FALLBACK_PACKAGES = new Set([
9
+ '@waica/engine',
10
+ '@waica/behaviors',
11
+ '@waica/archetype-platformer',
12
+ ]);
13
+ const FALLBACK_SPECIFIERS = new Set([...FALLBACK_PACKAGES, 'three']);
14
+ const REF_KINDS = new Set(['prefab', 'clip', 'action', 'stat']);
15
+ const RELATIVE_EXTENSIONS = ['.ts', '.tsx', '.js'];
16
+ function record(value) {
17
+ return value && typeof value === 'object' && !Array.isArray(value)
18
+ ? value
19
+ : {};
20
+ }
21
+ function installedPackage(projectPath, name) {
22
+ for (let current = projectPath;; current = path.dirname(current)) {
23
+ if (existsSync(path.join(current, 'node_modules', ...name.split('/'), 'package.json'))) {
24
+ return true;
25
+ }
26
+ const parent = path.dirname(current);
27
+ if (parent === current)
28
+ return false;
29
+ }
30
+ }
31
+ function relativeCandidates(unresolvedPath) {
32
+ const candidates = [
33
+ ...RELATIVE_EXTENSIONS.map((extension) => `${unresolvedPath}${extension}`),
34
+ ...RELATIVE_EXTENSIONS.map((extension) => path.join(unresolvedPath, `index${extension}`)),
35
+ ];
36
+ if (unresolvedPath.endsWith('.js')) {
37
+ const withoutJs = unresolvedPath.slice(0, -'.js'.length);
38
+ candidates.unshift(`${withoutJs}.ts`, `${withoutJs}.tsx`);
39
+ }
40
+ return candidates;
41
+ }
42
+ function resolveRelativeCandidate(unresolvedPath) {
43
+ return relativeCandidates(unresolvedPath).find((candidate) => existsSync(candidate));
44
+ }
45
+ function installProjectResolution(projectPath, fallbackEntries) {
46
+ if (typeof nodeModule.registerHooks !== 'function') {
47
+ throw new Error('node:module registerHooks is unavailable');
48
+ }
49
+ const projectRequire = createRequire(path.join(projectPath, 'package.json'));
50
+ // Resolve before registering the hook. Calling createRequire.resolve from
51
+ // inside a synchronous resolve hook recursively invokes that same hook.
52
+ // Failures stay attached to their package and surface only when an entry
53
+ // actually imports it, matching the previous per-package bridge behavior.
54
+ const packageEntries = new Map();
55
+ for (const specifier of FALLBACK_PACKAGES) {
56
+ try {
57
+ packageEntries.set(specifier, installedPackage(projectPath, specifier)
58
+ ? projectRequire.resolve(specifier)
59
+ : (fallbackEntries[specifier] ?? hostRequire.resolve(specifier)));
60
+ }
61
+ catch (error) {
62
+ packageEntries.set(specifier, error instanceof Error ? error : new Error(String(error)));
63
+ }
64
+ }
65
+ const fallbackRoots = [...FALLBACK_PACKAGES]
66
+ .map((specifier) => fallbackEntries[specifier])
67
+ .filter((entry) => typeof entry === 'string')
68
+ .map((entry) => realpathSync(path.dirname(entry)));
69
+ const fromFallback = (parentURL) => {
70
+ if (!parentURL?.startsWith('file:'))
71
+ return false;
72
+ const parent = realpathSync(fileURLToPath(parentURL));
73
+ return fallbackRoots.some((root) => parent === root || parent.startsWith(`${root}${path.sep}`));
74
+ };
75
+ nodeModule.registerHooks({
76
+ resolve(specifier, context, nextResolve) {
77
+ const dependencyEntry = fallbackEntries[specifier];
78
+ if (dependencyEntry && fromFallback(context.parentURL)) {
79
+ return { url: pathToFileURL(dependencyEntry).href, shortCircuit: true };
80
+ }
81
+ const packageEntry = packageEntries.get(specifier);
82
+ if (packageEntry instanceof Error)
83
+ throw packageEntry;
84
+ if (packageEntry) {
85
+ return { url: pathToFileURL(packageEntry).href, shortCircuit: true };
86
+ }
87
+ if (specifier.startsWith('.') && context.parentURL) {
88
+ const unresolved = new URL(specifier, context.parentURL);
89
+ try {
90
+ return nextResolve(specifier, context);
91
+ }
92
+ catch (error) {
93
+ const fallback = resolveRelativeCandidate(fileURLToPath(unresolved));
94
+ if (!fallback)
95
+ throw error;
96
+ return { url: pathToFileURL(fallback).href, shortCircuit: true };
97
+ }
98
+ }
99
+ return nextResolve(specifier, context);
100
+ },
101
+ });
102
+ }
103
+ function errorChain(error) {
104
+ const errors = [];
105
+ let current = error;
106
+ const seen = new Set();
107
+ while (current instanceof Error && !seen.has(current)) {
108
+ errors.push(current);
109
+ seen.add(current);
110
+ current = current.cause;
111
+ }
112
+ return errors;
113
+ }
114
+ function unsupportedByNode(error) {
115
+ for (const candidate of errorChain(error)) {
116
+ const code = candidate.code;
117
+ if (code === 'ERR_UNKNOWN_FILE_EXTENSION' ||
118
+ code === 'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX') {
119
+ return true;
120
+ }
121
+ if (/not supported in strip-only mode|unsupported TypeScript syntax|unknown file extension/i.test(candidate.message)) {
122
+ return true;
123
+ }
124
+ }
125
+ return false;
126
+ }
127
+ function causeText(error) {
128
+ if (!(error instanceof Error))
129
+ return String(error);
130
+ const code = error.code;
131
+ return `${code ? `${code}: ` : ''}${error.message}`;
132
+ }
133
+ function stringDefaults(Class) {
134
+ try {
135
+ return Object.fromEntries(Object.entries(new Class()).filter((entry) => typeof entry[1] === 'string'));
136
+ }
137
+ catch {
138
+ return {};
139
+ }
140
+ }
141
+ function componentRows(loaded, relativeFile) {
142
+ const rows = [];
143
+ for (const value of Object.values(loaded)) {
144
+ if (typeof value !== 'function' || !Object.hasOwn(value, 'componentName'))
145
+ continue;
146
+ const Class = value;
147
+ if (typeof Class.componentName !== 'string' || !Class.componentName)
148
+ continue;
149
+ const defaults = stringDefaults(Class);
150
+ const params = [];
151
+ for (const [name, rawSpec] of Object.entries(record(Class.params))) {
152
+ const spec = record(rawSpec);
153
+ if (typeof spec.ref !== 'string' || !REF_KINDS.has(spec.ref))
154
+ continue;
155
+ params.push({
156
+ name,
157
+ ref: spec.ref,
158
+ hasOptions: spec.options !== undefined,
159
+ ...(Object.hasOwn(defaults, name) ? { default: defaults[name] } : {}),
160
+ });
161
+ }
162
+ const hasUpdateAfter = Class.updateAfter !== undefined;
163
+ if (hasUpdateAfter &&
164
+ (!Array.isArray(Class.updateAfter) ||
165
+ Class.updateAfter.some((target) => typeof target !== 'string'))) {
166
+ throw new Error(`Component "${Class.componentName}" updateAfter must be an array of strings.`);
167
+ }
168
+ rows.push({
169
+ name: Class.componentName,
170
+ file: relativeFile,
171
+ params,
172
+ hasOnUpdate: typeof Class.prototype?.onUpdate === 'function',
173
+ hasUpdateAfter,
174
+ updateAfter: hasUpdateAfter ? [...Class.updateAfter] : [],
175
+ });
176
+ }
177
+ return rows;
178
+ }
179
+ function validRequest(value) {
180
+ const candidate = record(value);
181
+ const fallbacks = record(candidate.fallbackEntries);
182
+ return (candidate.kind === 'load-project-entry' &&
183
+ candidate.version === PROTOCOL_VERSION &&
184
+ typeof candidate.token === 'string' &&
185
+ candidate.token.length > 0 &&
186
+ typeof candidate.projectPath === 'string' &&
187
+ path.isAbsolute(candidate.projectPath) &&
188
+ typeof candidate.entryFile === 'string' &&
189
+ path.isAbsolute(candidate.entryFile) &&
190
+ typeof candidate.relativeFile === 'string' &&
191
+ candidate.relativeFile.length > 0 &&
192
+ !!candidate.fallbackEntries &&
193
+ typeof candidate.fallbackEntries === 'object' &&
194
+ !Array.isArray(candidate.fallbackEntries) &&
195
+ Object.entries(fallbacks).every(([specifier, entry]) => FALLBACK_SPECIFIERS.has(specifier) &&
196
+ typeof entry === 'string' &&
197
+ path.isAbsolute(entry)));
198
+ }
199
+ function sendTerminal(message) {
200
+ if (!process.send)
201
+ process.exit(1);
202
+ process.send(message, (error) => process.exit(error ? 1 : 0));
203
+ }
204
+ async function execute(request) {
205
+ try {
206
+ installProjectResolution(request.projectPath, request.fallbackEntries);
207
+ const loaded = (await import(pathToFileURL(request.entryFile).href));
208
+ sendTerminal({
209
+ kind: 'project-entry-result',
210
+ version: PROTOCOL_VERSION,
211
+ token: request.token,
212
+ ok: true,
213
+ components: componentRows(loaded, request.relativeFile),
214
+ });
215
+ }
216
+ catch (error) {
217
+ sendTerminal({
218
+ kind: 'project-entry-result',
219
+ version: PROTOCOL_VERSION,
220
+ token: request.token,
221
+ ok: false,
222
+ code: unsupportedByNode(error)
223
+ ? 'component-load-unsupported'
224
+ : 'component-load-failed',
225
+ message: causeText(error),
226
+ });
227
+ }
228
+ }
229
+ if (!process.send) {
230
+ throw new Error('Project component runner requires an IPC channel.');
231
+ }
232
+ process.send({ kind: 'project-entry-ready', version: PROTOCOL_VERSION });
233
+ process.once('message', (message) => {
234
+ if (!validRequest(message))
235
+ process.exit(1);
236
+ void execute(message);
237
+ });
@@ -0,0 +1,336 @@
1
+ import { chromium, } from 'playwright-core';
2
+ import { RuntimeToolError } from './runtime-service.js';
3
+ export const RUNTIME_BRIDGE_SYMBOL_KEY = '@waica/runtime-bridge/v1';
4
+ export const MINIMUM_RUNTIME_ENGINE_VERSION = '0.5.0';
5
+ /** Serialized by Playwright and installed before any Project module executes. */
6
+ export function installRuntimeBridgeActivation() {
7
+ const key = Symbol.for('@waica/runtime-bridge/v1');
8
+ const activation = {
9
+ protocolVersion: 1,
10
+ current: null,
11
+ failure: null,
12
+ register(bridge) {
13
+ if (this.current && this.current !== bridge) {
14
+ this.failure = {
15
+ code: 'multiple-games',
16
+ message: 'Exactly one live Game may register with a Run Session.',
17
+ };
18
+ return;
19
+ }
20
+ this.current = bridge;
21
+ },
22
+ unregister(bridge) {
23
+ if (this.current === bridge)
24
+ this.current = null;
25
+ },
26
+ };
27
+ Object.defineProperty(globalThis, key, {
28
+ configurable: true,
29
+ value: activation,
30
+ });
31
+ }
32
+ function boundedMessage(value) {
33
+ const text = value instanceof Error ? `${value.name}: ${value.message}` : String(value);
34
+ return text.length <= 4_096 ? text : `${text.slice(-4_096)}`;
35
+ }
36
+ function runtimeError(preflight, stage, message, diagnostics, code = 'runtime-start-failed') {
37
+ return new RuntimeToolError({
38
+ code,
39
+ stage,
40
+ message,
41
+ projectPath: preflight.projectPath,
42
+ ...(diagnostics ? { diagnostics } : {}),
43
+ });
44
+ }
45
+ function delay(milliseconds) {
46
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
47
+ }
48
+ async function readinessProbe(page) {
49
+ return page.evaluate(() => {
50
+ const hook = globalThis[Symbol.for('@waica/runtime-bridge/v1')];
51
+ if (!hook?.current) {
52
+ if (hook?.failure)
53
+ return { status: 'failure', message: hook.failure.message };
54
+ return { status: 'waiting' };
55
+ }
56
+ if (hook.failure)
57
+ return { status: 'failure', message: hook.failure.message };
58
+ try {
59
+ const bridge = hook.current;
60
+ return {
61
+ status: 'ready',
62
+ metadata: bridge.metadata(),
63
+ initialSnapshot: bridge.inspect(),
64
+ };
65
+ }
66
+ catch (error) {
67
+ return {
68
+ status: 'failure',
69
+ message: error instanceof Error ? error.message : String(error),
70
+ };
71
+ }
72
+ });
73
+ }
74
+ function bridgeReady(preflight, probe) {
75
+ const { metadata, initialSnapshot } = probe;
76
+ if (metadata.bridgeVersion !== 1) {
77
+ throw runtimeError(preflight, 'bridge', `Runtime Bridge protocol ${String(metadata.bridgeVersion)} is incompatible; protocol 1 is required.`, { minimumEngineVersion: MINIMUM_RUNTIME_ENGINE_VERSION }, 'runtime-incompatible');
78
+ }
79
+ if (metadata.mode !== 'paused' ||
80
+ metadata.frame !== 0 ||
81
+ metadata.simulationTime !== 0) {
82
+ throw runtimeError(preflight, 'game', 'The Runtime Bridge did not reach the required paused frame-zero baseline.', { metadata });
83
+ }
84
+ return {
85
+ engineVersion: metadata.engineVersion,
86
+ bridgeVersion: metadata.bridgeVersion,
87
+ mode: metadata.mode,
88
+ frame: metadata.frame,
89
+ simulationTime: metadata.simulationTime,
90
+ initialSnapshot,
91
+ };
92
+ }
93
+ class PlaywrightRuntimeBrowser {
94
+ preflight;
95
+ devServer;
96
+ browser;
97
+ context;
98
+ page;
99
+ lifecycle = {
100
+ reloading: () => { },
101
+ reloaded: () => { },
102
+ failed: () => { },
103
+ };
104
+ initialReady = false;
105
+ reloading = false;
106
+ closed = false;
107
+ readyValue;
108
+ browserErrors = [];
109
+ constructor(preflight, devServer, browser, context, page) {
110
+ this.preflight = preflight;
111
+ this.devServer = devServer;
112
+ this.browser = browser;
113
+ this.context = context;
114
+ this.page = page;
115
+ const recordError = (detail) => {
116
+ this.browserErrors.push(boundedMessage(detail));
117
+ if (this.browserErrors.length > 100)
118
+ this.browserErrors.shift();
119
+ };
120
+ page.on('pageerror', recordError);
121
+ page.on('console', (entry) => {
122
+ if (entry.type() === 'error')
123
+ recordError(entry.text());
124
+ });
125
+ page.on('crash', () => this.lifecycle.failed(new Error('Project page crashed.')));
126
+ page.on('close', () => {
127
+ if (!this.closed)
128
+ this.lifecycle.failed(new Error('Project page closed.'));
129
+ });
130
+ browser.on('disconnected', () => {
131
+ if (!this.closed)
132
+ this.lifecycle.failed(new Error('Browser disconnected.'));
133
+ });
134
+ page.on('framenavigated', (frame) => {
135
+ if (this.initialReady &&
136
+ frame === page.mainFrame() &&
137
+ !this.reloading &&
138
+ !this.closed) {
139
+ void this.handleReload();
140
+ }
141
+ });
142
+ }
143
+ async initialize() {
144
+ try {
145
+ await this.page.goto(this.devServer.url, {
146
+ waitUntil: 'load',
147
+ timeout: this.preflight.timeoutMs,
148
+ });
149
+ }
150
+ catch (error) {
151
+ throw runtimeError(this.preflight, 'page', `Could not load the Project page: ${boundedMessage(error)}`, this.diagnostics());
152
+ }
153
+ this.readyValue = await this.waitForReady();
154
+ this.initialReady = true;
155
+ }
156
+ async ready() {
157
+ if (!this.readyValue)
158
+ throw new Error('Runtime browser was not initialized.');
159
+ return this.readyValue;
160
+ }
161
+ async inspect(filters) {
162
+ return this.invokeBridge('inspect', {
163
+ ...(filters.entityIds ? { entity_ids: filters.entityIds } : {}),
164
+ ...(filters.entityNames ? { entity_names: filters.entityNames } : {}),
165
+ ...(filters.componentTypes ? { component_types: filters.componentTypes } : {}),
166
+ });
167
+ }
168
+ async control(request) {
169
+ return this.invokeBridge('control', request);
170
+ }
171
+ async captureScreenshot() {
172
+ this.assertOperational();
173
+ const geometry = await this.page.evaluate(() => {
174
+ const hook = globalThis[Symbol.for('@waica/runtime-bridge/v1')];
175
+ const bridge = hook.current;
176
+ const rect = bridge.surface.getBoundingClientRect();
177
+ return {
178
+ metadata: bridge.metadata(),
179
+ clip: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
180
+ };
181
+ });
182
+ const { width, height } = geometry.clip;
183
+ if (!Number.isFinite(width) ||
184
+ !Number.isFinite(height) ||
185
+ width <= 0 ||
186
+ height <= 0 ||
187
+ Math.ceil(width) * Math.ceil(height) > 1_000_000) {
188
+ throw runtimeError(this.preflight, 'game', 'The Game surface has invalid or oversized screenshot dimensions.', { clip: geometry.clip }, 'runtime-operation-failed');
189
+ }
190
+ const png = await this.page.screenshot({
191
+ type: 'png',
192
+ clip: geometry.clip,
193
+ animations: 'allow',
194
+ caret: 'hide',
195
+ });
196
+ const metadata = await this.invokeBridge('metadata', {});
197
+ return { ...metadata, data: png.toString('base64') };
198
+ }
199
+ async close() {
200
+ if (this.closed)
201
+ return;
202
+ this.closed = true;
203
+ await this.context.close().catch(() => { });
204
+ await this.browser.close().catch(() => { });
205
+ }
206
+ setLifecycleHandlers(handlers) {
207
+ this.lifecycle = handlers;
208
+ }
209
+ async waitForReady() {
210
+ const deadline = Date.now() + this.preflight.timeoutMs;
211
+ while (Date.now() <= deadline) {
212
+ if (this.closed || this.page.isClosed()) {
213
+ throw runtimeError(this.preflight, 'page', 'The Project page closed before Runtime Bridge readiness.', this.diagnostics());
214
+ }
215
+ const probe = await readinessProbe(this.page).catch((error) => ({
216
+ status: 'failure',
217
+ message: boundedMessage(error),
218
+ }));
219
+ if (probe.status === 'ready')
220
+ return bridgeReady(this.preflight, probe);
221
+ if (probe.status === 'failure') {
222
+ throw runtimeError(this.preflight, probe.message.includes('Exactly one live Game') ? 'game' : 'bridge', probe.message, this.diagnostics());
223
+ }
224
+ await delay(25);
225
+ }
226
+ const diagnostics = this.diagnostics();
227
+ if (this.browserErrors.length > 0) {
228
+ throw runtimeError(this.preflight, 'page', 'The Project page did not reach Runtime Bridge readiness.', diagnostics);
229
+ }
230
+ throw runtimeError(this.preflight, 'bridge', 'Timed out waiting for Runtime Bridge protocol 1 and one live Game.', { ...diagnostics, minimumEngineVersion: MINIMUM_RUNTIME_ENGINE_VERSION }, this.preflight.engine.version === MINIMUM_RUNTIME_ENGINE_VERSION
231
+ ? 'runtime-start-failed'
232
+ : 'runtime-incompatible');
233
+ }
234
+ async invokeBridge(operation, argument) {
235
+ this.assertOperational();
236
+ const response = await this.page.evaluate(({ operation, argument }) => {
237
+ const hook = globalThis[Symbol.for('@waica/runtime-bridge/v1')];
238
+ if (!hook?.current || hook.failure) {
239
+ return {
240
+ ok: false,
241
+ error: {
242
+ code: 'runtime-invalid-state',
243
+ stage: 'game',
244
+ message: hook?.failure?.message ?? 'No live Game is registered.',
245
+ },
246
+ };
247
+ }
248
+ try {
249
+ const bridge = hook.current;
250
+ const value = operation === 'inspect'
251
+ ? bridge.inspect(argument)
252
+ : operation === 'control'
253
+ ? bridge.control(argument)
254
+ : bridge.metadata();
255
+ return { ok: true, value };
256
+ }
257
+ catch (error) {
258
+ const detail = error;
259
+ return {
260
+ ok: false,
261
+ error: {
262
+ code: typeof detail.code === 'string' ? detail.code : 'runtime-operation-failed',
263
+ stage: typeof detail.stage === 'string' ? detail.stage : 'control',
264
+ message: typeof detail.message === 'string' ? detail.message : String(error),
265
+ ...(Array.isArray(detail.availableActions)
266
+ ? { availableActions: detail.availableActions }
267
+ : {}),
268
+ },
269
+ };
270
+ }
271
+ }, { operation, argument });
272
+ if (response.ok && response.value)
273
+ return response.value;
274
+ const error = response.error ?? {
275
+ code: 'runtime-operation-failed',
276
+ stage: 'control',
277
+ message: 'Runtime Bridge operation failed.',
278
+ };
279
+ throw runtimeError(this.preflight, error.stage === 'game' ? 'game' : 'control', error.message, error.availableActions ? { availableActions: error.availableActions } : undefined, error.code === 'runtime-invalid-state'
280
+ ? 'runtime-invalid-state'
281
+ : 'runtime-operation-failed');
282
+ }
283
+ assertOperational() {
284
+ if (this.closed || this.page.isClosed()) {
285
+ throw runtimeError(this.preflight, 'game', 'The Project browser page is not available.', this.diagnostics(), 'runtime-invalid-state');
286
+ }
287
+ }
288
+ diagnostics() {
289
+ return {
290
+ ...this.devServer.diagnostics(),
291
+ browserErrors: [...this.browserErrors],
292
+ };
293
+ }
294
+ async handleReload() {
295
+ this.reloading = true;
296
+ this.browserErrors.length = 0;
297
+ this.lifecycle.reloading();
298
+ try {
299
+ const next = await this.waitForReady();
300
+ this.readyValue = next;
301
+ this.lifecycle.reloaded(next);
302
+ }
303
+ catch (error) {
304
+ this.lifecycle.failed(error);
305
+ }
306
+ finally {
307
+ this.reloading = false;
308
+ }
309
+ }
310
+ }
311
+ export async function startRuntimeBrowser(preflight, devServer) {
312
+ let browser;
313
+ let context;
314
+ try {
315
+ browser = await chromium.launch({
316
+ executablePath: preflight.browserExecutablePath,
317
+ headless: preflight.headless,
318
+ });
319
+ context = await browser.newContext({
320
+ viewport: preflight.viewport,
321
+ deviceScaleFactor: 1,
322
+ });
323
+ await context.addInitScript(installRuntimeBridgeActivation);
324
+ const page = await context.newPage();
325
+ const runtime = new PlaywrightRuntimeBrowser(preflight, devServer, browser, context, page);
326
+ await runtime.initialize();
327
+ return runtime;
328
+ }
329
+ catch (error) {
330
+ await context?.close().catch(() => { });
331
+ await browser?.close().catch(() => { });
332
+ if (error instanceof RuntimeToolError)
333
+ throw error;
334
+ throw runtimeError(preflight, browser ? 'page' : 'browser', `Could not launch a compatible browser context: ${boundedMessage(error)}`, devServer.diagnostics());
335
+ }
336
+ }