pixifact-cli 0.4.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.
- package/README.md +40 -1
- package/editor/assets/browserAll-DCAvtrKG.js +1 -0
- package/editor/assets/index-D8cbY_GQ.js +1130 -0
- package/editor/assets/index-gCKoe0w8.css +1 -0
- package/editor/assets/init-ClFt_GA_.js +1 -0
- package/editor/assets/webworkerAll-Do_n5w1y.js +1 -0
- package/editor/index.html +13 -0
- package/package.json +4 -3
- package/src/automation.ts +17 -9
- package/src/editorServer.ts +375 -0
- package/src/editorSession.ts +697 -0
- package/src/pixifact-cli.ts +180 -50
- package/src/runtimeSession.ts +133 -0
- package/src/liveBridge.ts +0 -28
- package/src/liveBridgeServer.ts +0 -163
package/src/pixifact-cli.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
2
4
|
import { createPixifactAutomation } from './automation';
|
|
3
5
|
import { hintForCommandError } from 'pixifact';
|
|
4
6
|
import { CompileSceneError, compileScenes } from 'pixifact/compiler-node';
|
|
5
7
|
import type { SceneValidationDiagnostic } from 'pixifact/compiler';
|
|
6
|
-
import {
|
|
8
|
+
import { startEditorServer } from './editorServer';
|
|
9
|
+
import { captureEditorScreenshot, queryEditorContext } from './editorSession';
|
|
10
|
+
import { queryRuntime, queryRuntimeList } from './runtimeSession';
|
|
11
|
+
import type { RuntimeLogLevel, RuntimeRequest } from 'pixifact/runtime-dev';
|
|
7
12
|
import {
|
|
8
13
|
buildWechatTarget,
|
|
9
14
|
devWechatTarget,
|
|
@@ -13,14 +18,15 @@ import {
|
|
|
13
18
|
} from './wechatTarget';
|
|
14
19
|
|
|
15
20
|
type Automation = ReturnType<typeof createPixifactAutomation>;
|
|
16
|
-
type LiveBridge = Pick<ReturnType<typeof createLiveBridgeServer>, 'connected' | 'callAction' | 'stop'> & {
|
|
17
|
-
waitForConnection?: (timeoutMs?: number) => Promise<void>;
|
|
18
|
-
};
|
|
19
21
|
|
|
20
22
|
interface CliOptions {
|
|
21
23
|
automation?: Automation;
|
|
22
|
-
|
|
24
|
+
captureEditorScreenshot?: typeof captureEditorScreenshot;
|
|
23
25
|
onWechatDevEvent?: (event: WechatDevEvent) => void;
|
|
26
|
+
readEditorContext?: typeof queryEditorContext;
|
|
27
|
+
listRuntimes?: typeof queryRuntimeList;
|
|
28
|
+
queryRuntime?: typeof queryRuntime;
|
|
29
|
+
startEditor?: typeof startEditorServer;
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
interface CliResult {
|
|
@@ -83,6 +89,23 @@ function projectRootFlag(flags: Record<string, string | true>) {
|
|
|
83
89
|
return requireFlag(flags, 'project-root');
|
|
84
90
|
}
|
|
85
91
|
|
|
92
|
+
function optionalFlag(flags: Record<string, string | true>, name: string) {
|
|
93
|
+
const value = flags[name];
|
|
94
|
+
return value === undefined ? undefined : requireFlag(flags, name);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function finiteNumberFlag(flags: Record<string, string | true>, name: string) {
|
|
98
|
+
const value = Number(requireFlag(flags, name));
|
|
99
|
+
if (!Number.isFinite(value)) throw new Error(`--${name} must be a finite number.`);
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function nonNegativeInteger(value: string | undefined, error: string) {
|
|
104
|
+
const number = Number(value);
|
|
105
|
+
if (!Number.isInteger(number) || number < 0) throw new Error(error);
|
|
106
|
+
return number;
|
|
107
|
+
}
|
|
108
|
+
|
|
86
109
|
function jsonLine(value: unknown) {
|
|
87
110
|
return `${JSON.stringify(value, null, 2)}\n`;
|
|
88
111
|
}
|
|
@@ -93,9 +116,25 @@ function isFailedResult(value: unknown): value is CliJsonResult {
|
|
|
93
116
|
&& (value as CliJsonResult).ok === false;
|
|
94
117
|
}
|
|
95
118
|
|
|
119
|
+
function isRuntimeTreeResult(value: unknown): value is { runtimeId: string; root: unknown } {
|
|
120
|
+
return typeof value === 'object'
|
|
121
|
+
&& value !== null
|
|
122
|
+
&& typeof (value as { runtimeId?: unknown }).runtimeId === 'string'
|
|
123
|
+
&& 'root' in value;
|
|
124
|
+
}
|
|
125
|
+
|
|
96
126
|
function compileScenesFailure(error: unknown): CliJsonResult {
|
|
97
127
|
const message = error instanceof Error ? error.message : String(error);
|
|
98
128
|
if (error instanceof CompileSceneError) {
|
|
129
|
+
if (error.diagnostics) {
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
scene: error.scene,
|
|
133
|
+
error: 'Scene compile failed.',
|
|
134
|
+
diagnostics: error.diagnostics,
|
|
135
|
+
hint: 'Fix the listed diagnostics, then run compile-scenes again.',
|
|
136
|
+
};
|
|
137
|
+
}
|
|
99
138
|
const sourceDiagnostic = sourceDiagnosticFromMessage(error.source ?? '', message);
|
|
100
139
|
if (sourceDiagnostic) {
|
|
101
140
|
return {
|
|
@@ -218,10 +257,119 @@ async function executeFileCommand(
|
|
|
218
257
|
flags: Record<string, string | true>,
|
|
219
258
|
automation: Automation,
|
|
220
259
|
onWechatDevEvent?: (event: WechatDevEvent) => void,
|
|
260
|
+
startEditor: typeof startEditorServer = startEditorServer,
|
|
261
|
+
readEditorContext: typeof queryEditorContext = queryEditorContext,
|
|
262
|
+
captureScreenshot: typeof captureEditorScreenshot = captureEditorScreenshot,
|
|
263
|
+
listRuntimes: typeof queryRuntimeList = queryRuntimeList,
|
|
264
|
+
sendRuntimeRequest: typeof queryRuntime = queryRuntime,
|
|
221
265
|
) {
|
|
222
266
|
const [area, action] = positionals;
|
|
223
267
|
const projectRoot = projectRootFlag(flags);
|
|
224
268
|
|
|
269
|
+
if (area === 'runtime' && action === 'list') {
|
|
270
|
+
return listRuntimes({ projectRoot });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (area === 'runtime' && action !== undefined) {
|
|
274
|
+
const runtimeId = optionalFlag(flags, 'runtime');
|
|
275
|
+
let request: RuntimeRequest;
|
|
276
|
+
if (action === 'tree') {
|
|
277
|
+
request = { type: 'tree' };
|
|
278
|
+
} else if (action === 'node') {
|
|
279
|
+
request = {
|
|
280
|
+
type: 'node',
|
|
281
|
+
uid: nonNegativeInteger(
|
|
282
|
+
positionals[2],
|
|
283
|
+
'Runtime node uid must be a non-negative integer.',
|
|
284
|
+
),
|
|
285
|
+
};
|
|
286
|
+
} else if (action === 'state') {
|
|
287
|
+
request = { type: 'state' };
|
|
288
|
+
} else if (action === 'logs') {
|
|
289
|
+
const after = flags.after === undefined
|
|
290
|
+
? undefined
|
|
291
|
+
: nonNegativeInteger(requireFlag(flags, 'after'), '--after must be a non-negative integer.');
|
|
292
|
+
const level = optionalFlag(flags, 'level');
|
|
293
|
+
if (level && !['debug', 'log', 'info', 'warn', 'error'].includes(level)) {
|
|
294
|
+
throw new Error('--level must be debug, log, info, warn, or error.');
|
|
295
|
+
}
|
|
296
|
+
request = {
|
|
297
|
+
type: 'logs',
|
|
298
|
+
...(after === undefined ? {} : { after }),
|
|
299
|
+
...(level === undefined ? {} : { level: level as RuntimeLogLevel }),
|
|
300
|
+
};
|
|
301
|
+
} else if (action === 'input') {
|
|
302
|
+
const inputAction = positionals[2];
|
|
303
|
+
if (inputAction === 'click' || inputAction === 'move') {
|
|
304
|
+
request = {
|
|
305
|
+
type: 'input',
|
|
306
|
+
action: inputAction,
|
|
307
|
+
x: finiteNumberFlag(flags, 'x'),
|
|
308
|
+
y: finiteNumberFlag(flags, 'y'),
|
|
309
|
+
};
|
|
310
|
+
} else if (inputAction === 'key' || inputAction === 'keydown' || inputAction === 'keyup') {
|
|
311
|
+
const key = positionals[3];
|
|
312
|
+
if (!key) throw new Error(`runtime input ${inputAction} requires a key.`);
|
|
313
|
+
request = { type: 'input', action: inputAction, key };
|
|
314
|
+
} else {
|
|
315
|
+
throw new Error(`Unknown Pixifact Runtime input "${inputAction ?? ''}".`);
|
|
316
|
+
}
|
|
317
|
+
} else {
|
|
318
|
+
throw new Error(`Unknown Pixifact CLI command "${positionals.join(' ')}".`);
|
|
319
|
+
}
|
|
320
|
+
const result = await sendRuntimeRequest({ projectRoot, runtimeId, request });
|
|
321
|
+
if (action !== 'tree' || flags.output === undefined || isFailedResult(result)) {
|
|
322
|
+
return result;
|
|
323
|
+
}
|
|
324
|
+
if (!isRuntimeTreeResult(result)) {
|
|
325
|
+
throw new Error('Pixifact Runtime returned an invalid tree response.');
|
|
326
|
+
}
|
|
327
|
+
const output = path.resolve(requireFlag(flags, 'output'));
|
|
328
|
+
const capturedAt = new Date().toISOString();
|
|
329
|
+
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
330
|
+
fs.writeFileSync(output, jsonLine({
|
|
331
|
+
schemaVersion: 1,
|
|
332
|
+
capturedAt,
|
|
333
|
+
runtimeId: result.runtimeId,
|
|
334
|
+
root: result.root,
|
|
335
|
+
}), 'utf8');
|
|
336
|
+
return {
|
|
337
|
+
ok: true,
|
|
338
|
+
output,
|
|
339
|
+
runtimeId: result.runtimeId,
|
|
340
|
+
capturedAt,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (area === 'editor' && action === undefined) {
|
|
345
|
+
const session = await startEditor({ projectRoot });
|
|
346
|
+
return {
|
|
347
|
+
ok: true,
|
|
348
|
+
projectRoot,
|
|
349
|
+
url: session.url,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (area === 'editor' && action === 'context') {
|
|
354
|
+
return readEditorContext({ projectRoot });
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (area === 'editor' && action === 'screenshot') {
|
|
358
|
+
const output = path.resolve(requireFlag(flags, 'output'));
|
|
359
|
+
const screenshot = await captureScreenshot({ projectRoot });
|
|
360
|
+
if (!screenshot.ok) return screenshot;
|
|
361
|
+
fs.writeFileSync(output, screenshot.data);
|
|
362
|
+
return {
|
|
363
|
+
ok: true,
|
|
364
|
+
scene: screenshot.scene,
|
|
365
|
+
revision: screenshot.revision,
|
|
366
|
+
width: screenshot.width,
|
|
367
|
+
height: screenshot.height,
|
|
368
|
+
bytes: screenshot.data.byteLength,
|
|
369
|
+
output,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
225
373
|
if ((area === 'build' || area === 'dev' || area === 'validate') && action === undefined) {
|
|
226
374
|
const target = requireFlag(flags, 'target');
|
|
227
375
|
if (target !== 'wechat') {
|
|
@@ -350,32 +498,7 @@ async function executeFileCommand(
|
|
|
350
498
|
throw new Error(`Unknown Pixifact CLI command "${positionals.join(' ')}".`);
|
|
351
499
|
}
|
|
352
500
|
|
|
353
|
-
async function executeLiveCommand(positionals: string[], flags: Record<string, string | true>, bridge: LiveBridge) {
|
|
354
|
-
if (!bridge.connected && bridge.waitForConnection) {
|
|
355
|
-
await bridge.waitForConnection();
|
|
356
|
-
}
|
|
357
|
-
if (!bridge.connected) {
|
|
358
|
-
throw new Error('No live Pixifact editor is connected.');
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const [area, action] = positionals;
|
|
362
|
-
if (area === 'summary' && action === undefined) {
|
|
363
|
-
return bridge.callAction('summary', {});
|
|
364
|
-
}
|
|
365
|
-
if (area === 'scene' && action === 'get') {
|
|
366
|
-
return bridge.callAction('scene.get', {});
|
|
367
|
-
}
|
|
368
|
-
if (area === 'node' && action === 'inspect') {
|
|
369
|
-
return bridge.callAction('node.inspect', {
|
|
370
|
-
node: requireFlag(flags, 'node'),
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
throw new Error(`Unknown Pixifact live command "${positionals.join(' ')}".`);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
501
|
export async function executePixifactCli(argv: string[], options: CliOptions = {}): Promise<CliResult> {
|
|
378
|
-
let ownedBridge: ReturnType<typeof createLiveBridgeServer> | undefined;
|
|
379
502
|
try {
|
|
380
503
|
const parsed = parseArgs(argv);
|
|
381
504
|
if (parsed.flags.help === true) {
|
|
@@ -394,15 +517,26 @@ export async function executePixifactCli(argv: string[], options: CliOptions = {
|
|
|
394
517
|
aiValidationAlternatives: [
|
|
395
518
|
'scene validate --all',
|
|
396
519
|
],
|
|
520
|
+
runtimeCommands: [
|
|
521
|
+
'runtime list',
|
|
522
|
+
'runtime tree [--output <json-path>] [--runtime <runtime-id>]',
|
|
523
|
+
'runtime node <pixi-uid> [--runtime <runtime-id>]',
|
|
524
|
+
'runtime state [--runtime <runtime-id>]',
|
|
525
|
+
'runtime logs [--after <seq>] [--level <level>] [--runtime <runtime-id>]',
|
|
526
|
+
'runtime input click --x <x> --y <y> [--runtime <runtime-id>]',
|
|
527
|
+
'runtime input move --x <x> --y <y> [--runtime <runtime-id>]',
|
|
528
|
+
'runtime input key <key> [--runtime <runtime-id>]',
|
|
529
|
+
'runtime input keydown <key> [--runtime <runtime-id>]',
|
|
530
|
+
'runtime input keyup <key> [--runtime <runtime-id>]',
|
|
531
|
+
],
|
|
397
532
|
auxiliaryCommands: [
|
|
533
|
+
'editor',
|
|
534
|
+
'editor context',
|
|
535
|
+
'editor screenshot --output <png-path>',
|
|
398
536
|
'scene create --scene <scene-path> --name <SceneName>',
|
|
399
537
|
'node inspect --scene <scene-path> --node <locator>',
|
|
400
538
|
],
|
|
401
|
-
|
|
402
|
-
'live summary',
|
|
403
|
-
'live scene get',
|
|
404
|
-
'live node inspect --node <locator>',
|
|
405
|
-
],
|
|
539
|
+
nodeLocatorSource: 'scene inspect --scene <scene-path> returns node locator values',
|
|
406
540
|
defaults: {
|
|
407
541
|
projectRoot: 'current working directory',
|
|
408
542
|
},
|
|
@@ -412,19 +546,17 @@ export async function executePixifactCli(argv: string[], options: CliOptions = {
|
|
|
412
546
|
}
|
|
413
547
|
|
|
414
548
|
const automation = options.automation ?? createPixifactAutomation();
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
options.onWechatDevEvent,
|
|
427
|
-
);
|
|
549
|
+
const result = await executeFileCommand(
|
|
550
|
+
parsed.positionals,
|
|
551
|
+
parsed.flags,
|
|
552
|
+
automation,
|
|
553
|
+
options.onWechatDevEvent,
|
|
554
|
+
options.startEditor,
|
|
555
|
+
options.readEditorContext,
|
|
556
|
+
options.captureEditorScreenshot,
|
|
557
|
+
options.listRuntimes,
|
|
558
|
+
options.queryRuntime,
|
|
559
|
+
);
|
|
428
560
|
if (isFailedResult(result)) {
|
|
429
561
|
return {
|
|
430
562
|
exitCode: 1,
|
|
@@ -447,8 +579,6 @@ export async function executePixifactCli(argv: string[], options: CliOptions = {
|
|
|
447
579
|
hint: hintForCommandError(error instanceof Error ? error.message : String(error)),
|
|
448
580
|
}),
|
|
449
581
|
};
|
|
450
|
-
} finally {
|
|
451
|
-
ownedBridge?.stop();
|
|
452
582
|
}
|
|
453
583
|
}
|
|
454
584
|
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import {
|
|
2
|
+
findActiveRuntimeSession,
|
|
3
|
+
type RuntimeSessionDescriptor,
|
|
4
|
+
} from 'pixifact/compiler-node';
|
|
5
|
+
import type { RuntimePageDescriptor, RuntimeRequest } from 'pixifact/runtime-dev';
|
|
6
|
+
|
|
7
|
+
interface RuntimeSessionLookupOptions {
|
|
8
|
+
projectRoot: string;
|
|
9
|
+
sessionsRoot?: string;
|
|
10
|
+
fetch?: typeof fetch;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface RuntimeQueryOptions extends RuntimeSessionLookupOptions {
|
|
14
|
+
runtimeId?: string;
|
|
15
|
+
request: RuntimeRequest;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type RuntimeListResult =
|
|
19
|
+
| { ok: true; runtimes: Array<RuntimePageDescriptor & { connectedAt: string }> }
|
|
20
|
+
| { ok: false; error: string; [key: string]: unknown };
|
|
21
|
+
|
|
22
|
+
async function activeDescriptor(options: RuntimeSessionLookupOptions) {
|
|
23
|
+
const descriptor = await findActiveRuntimeSession(options);
|
|
24
|
+
if (!descriptor) {
|
|
25
|
+
return {
|
|
26
|
+
ok: false as const,
|
|
27
|
+
error: 'No Pixifact Runtime Vite server is running for this project.',
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return { ok: true as const, descriptor };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function runtimeHostFetch(
|
|
34
|
+
descriptor: RuntimeSessionDescriptor,
|
|
35
|
+
pathname: string,
|
|
36
|
+
fetcher: typeof fetch,
|
|
37
|
+
init: RequestInit = {},
|
|
38
|
+
) {
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetcher(`${descriptor.origin}/__pixifact_runtime__/${pathname}`, {
|
|
41
|
+
...init,
|
|
42
|
+
headers: {
|
|
43
|
+
authorization: `Bearer ${descriptor.token}`,
|
|
44
|
+
...init.headers,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
return await response.json() as Record<string, unknown>;
|
|
48
|
+
} catch {
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
error: 'Pixifact Runtime Vite server is not reachable for this project.',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function queryRuntimeList(
|
|
57
|
+
options: RuntimeSessionLookupOptions,
|
|
58
|
+
): Promise<RuntimeListResult> {
|
|
59
|
+
const active = await activeDescriptor(options);
|
|
60
|
+
if (!active.ok) return active;
|
|
61
|
+
const result = await runtimeHostFetch(active.descriptor, 'list', options.fetch ?? fetch);
|
|
62
|
+
if (
|
|
63
|
+
result.ok !== true
|
|
64
|
+
|| !Array.isArray(result.runtimes)
|
|
65
|
+
|| !result.runtimes.every((runtime) => (
|
|
66
|
+
typeof runtime === 'object'
|
|
67
|
+
&& runtime !== null
|
|
68
|
+
&& typeof runtime.runtimeId === 'string'
|
|
69
|
+
&& typeof runtime.url === 'string'
|
|
70
|
+
&& typeof runtime.title === 'string'
|
|
71
|
+
&& typeof runtime.ready === 'boolean'
|
|
72
|
+
&& typeof runtime.connectedAt === 'string'
|
|
73
|
+
))
|
|
74
|
+
) {
|
|
75
|
+
return result.ok === false && typeof result.error === 'string'
|
|
76
|
+
? result as RuntimeListResult
|
|
77
|
+
: {
|
|
78
|
+
ok: false,
|
|
79
|
+
error: 'Pixifact Runtime Vite server returned an invalid runtime list.',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return result as RuntimeListResult;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function selectRuntime(
|
|
86
|
+
runtimes: Array<RuntimePageDescriptor & { connectedAt: string }>,
|
|
87
|
+
runtimeId: string | undefined,
|
|
88
|
+
) {
|
|
89
|
+
if (runtimeId) {
|
|
90
|
+
const selected = runtimes.find((runtime) => runtime.runtimeId === runtimeId);
|
|
91
|
+
return selected
|
|
92
|
+
? { ok: true as const, runtime: selected }
|
|
93
|
+
: {
|
|
94
|
+
ok: false as const,
|
|
95
|
+
error: `Pixifact Runtime "${runtimeId}" is not connected.`,
|
|
96
|
+
runtimes,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (runtimes.length === 0) {
|
|
100
|
+
return {
|
|
101
|
+
ok: false as const,
|
|
102
|
+
error: 'No Pixifact Runtime game page is connected for this project.',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (runtimes.length > 1) {
|
|
106
|
+
return {
|
|
107
|
+
ok: false as const,
|
|
108
|
+
error: 'Multiple Pixifact Runtime game pages are connected. Use --runtime <runtime-id>.',
|
|
109
|
+
runtimes,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return { ok: true as const, runtime: runtimes[0] };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function queryRuntime(options: RuntimeQueryOptions) {
|
|
116
|
+
const active = await activeDescriptor(options);
|
|
117
|
+
if (!active.ok) return active;
|
|
118
|
+
const listed = await runtimeHostFetch(active.descriptor, 'list', options.fetch ?? fetch);
|
|
119
|
+
if (listed.ok !== true || !Array.isArray(listed.runtimes)) return listed;
|
|
120
|
+
const selected = selectRuntime(
|
|
121
|
+
listed.runtimes as Array<RuntimePageDescriptor & { connectedAt: string }>,
|
|
122
|
+
options.runtimeId,
|
|
123
|
+
);
|
|
124
|
+
if (!selected.ok) return selected;
|
|
125
|
+
return runtimeHostFetch(active.descriptor, 'request', options.fetch ?? fetch, {
|
|
126
|
+
method: 'POST',
|
|
127
|
+
headers: { 'content-type': 'application/json' },
|
|
128
|
+
body: JSON.stringify({
|
|
129
|
+
runtimeId: selected.runtime.runtimeId,
|
|
130
|
+
request: options.request,
|
|
131
|
+
}),
|
|
132
|
+
});
|
|
133
|
+
}
|
package/src/liveBridge.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
export const pixifactAgentBridgePort = 8791;
|
|
2
|
-
export const pixifactAgentBridgePath = '/pixifact-agent';
|
|
3
|
-
export const pixifactAgentBridgeUrl = `ws://127.0.0.1:${pixifactAgentBridgePort}${pixifactAgentBridgePath}`;
|
|
4
|
-
|
|
5
|
-
export type LiveBridgeRole = 'editor';
|
|
6
|
-
|
|
7
|
-
export interface LiveBridgeHelloMessage {
|
|
8
|
-
type: 'hello';
|
|
9
|
-
role: LiveBridgeRole;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface LiveBridgeRequestMessage {
|
|
13
|
-
type: 'request';
|
|
14
|
-
id: string;
|
|
15
|
-
action: string;
|
|
16
|
-
arguments: unknown;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface LiveBridgeResponseMessage {
|
|
20
|
-
type: 'response';
|
|
21
|
-
id: string;
|
|
22
|
-
ok: boolean;
|
|
23
|
-
result?: unknown;
|
|
24
|
-
error?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export type LiveBridgeClientMessage = LiveBridgeHelloMessage | LiveBridgeResponseMessage;
|
|
28
|
-
export type LiveBridgeServerMessage = LiveBridgeRequestMessage;
|
package/src/liveBridgeServer.ts
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
pixifactAgentBridgePath,
|
|
3
|
-
pixifactAgentBridgePort,
|
|
4
|
-
type LiveBridgeClientMessage,
|
|
5
|
-
type LiveBridgeRequestMessage,
|
|
6
|
-
type LiveBridgeResponseMessage,
|
|
7
|
-
} from './liveBridge';
|
|
8
|
-
|
|
9
|
-
interface BunServerSocket {
|
|
10
|
-
send(data: string): void;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface BunServer {
|
|
14
|
-
upgrade(request: Request): boolean;
|
|
15
|
-
stop(force?: boolean): void;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface BunServeOptions {
|
|
19
|
-
hostname: string;
|
|
20
|
-
port: number;
|
|
21
|
-
fetch(request: Request, server: BunServer): Response | undefined;
|
|
22
|
-
websocket: {
|
|
23
|
-
message(socket: BunServerSocket, data: string | Uint8Array): void;
|
|
24
|
-
close(socket: BunServerSocket): void;
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
declare const Bun: {
|
|
29
|
-
serve(options: BunServeOptions): BunServer;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
interface PendingRequest {
|
|
33
|
-
resolve(result: unknown): void;
|
|
34
|
-
reject(error: Error): void;
|
|
35
|
-
timer: ReturnType<typeof setTimeout>;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
interface LiveBridgeServerOptions {
|
|
39
|
-
port?: number;
|
|
40
|
-
requestTimeoutMs?: number;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function responseError(message: LiveBridgeResponseMessage) {
|
|
44
|
-
return new Error(message.error ?? 'Live editor request failed.');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function createLiveBridgeServer(options: LiveBridgeServerOptions = {}) {
|
|
48
|
-
const port = options.port ?? pixifactAgentBridgePort;
|
|
49
|
-
const requestTimeoutMs = options.requestTimeoutMs ?? 30000;
|
|
50
|
-
let editor: BunServerSocket | undefined;
|
|
51
|
-
const pending = new Map<string, PendingRequest>();
|
|
52
|
-
const connectionWaiters = new Set<() => void>();
|
|
53
|
-
let nextRequestId = 0;
|
|
54
|
-
|
|
55
|
-
function rejectPending(error: Error) {
|
|
56
|
-
for (const pendingRequest of pending.values()) {
|
|
57
|
-
clearTimeout(pendingRequest.timer);
|
|
58
|
-
pendingRequest.reject(error);
|
|
59
|
-
}
|
|
60
|
-
pending.clear();
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const server = Bun.serve({
|
|
64
|
-
hostname: '127.0.0.1',
|
|
65
|
-
port,
|
|
66
|
-
fetch(request, server) {
|
|
67
|
-
const url = new URL(request.url);
|
|
68
|
-
if (url.pathname !== pixifactAgentBridgePath) {
|
|
69
|
-
return new Response('Not found.', { status: 404 });
|
|
70
|
-
}
|
|
71
|
-
if (server.upgrade(request)) {
|
|
72
|
-
return undefined;
|
|
73
|
-
}
|
|
74
|
-
return new Response('WebSocket upgrade failed.', { status: 400 });
|
|
75
|
-
},
|
|
76
|
-
websocket: {
|
|
77
|
-
message(socket, data) {
|
|
78
|
-
const message = JSON.parse(String(data)) as LiveBridgeClientMessage;
|
|
79
|
-
if (message.type === 'hello' && message.role === 'editor') {
|
|
80
|
-
editor = socket;
|
|
81
|
-
for (const resolve of connectionWaiters) {
|
|
82
|
-
resolve();
|
|
83
|
-
}
|
|
84
|
-
connectionWaiters.clear();
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
if (message.type !== 'response') {
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
const pendingRequest = pending.get(message.id);
|
|
91
|
-
if (!pendingRequest) {
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
pending.delete(message.id);
|
|
95
|
-
clearTimeout(pendingRequest.timer);
|
|
96
|
-
if (message.ok) {
|
|
97
|
-
pendingRequest.resolve(message.result);
|
|
98
|
-
} else {
|
|
99
|
-
pendingRequest.reject(responseError(message));
|
|
100
|
-
}
|
|
101
|
-
},
|
|
102
|
-
close(socket) {
|
|
103
|
-
if (socket === editor) {
|
|
104
|
-
editor = undefined;
|
|
105
|
-
rejectPending(new Error('Live editor disconnected.'));
|
|
106
|
-
}
|
|
107
|
-
},
|
|
108
|
-
},
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
return {
|
|
112
|
-
get connected() {
|
|
113
|
-
return editor !== undefined;
|
|
114
|
-
},
|
|
115
|
-
|
|
116
|
-
async waitForConnection(timeoutMs = 3000) {
|
|
117
|
-
if (editor) {
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
return new Promise<void>((resolve, reject) => {
|
|
121
|
-
const timer = setTimeout(() => {
|
|
122
|
-
connectionWaiters.delete(done);
|
|
123
|
-
reject(new Error('No live Pixifact editor is connected.'));
|
|
124
|
-
}, timeoutMs);
|
|
125
|
-
function done() {
|
|
126
|
-
clearTimeout(timer);
|
|
127
|
-
resolve();
|
|
128
|
-
}
|
|
129
|
-
connectionWaiters.add(done);
|
|
130
|
-
});
|
|
131
|
-
},
|
|
132
|
-
|
|
133
|
-
async callAction(action: string, args: unknown) {
|
|
134
|
-
if (!editor) {
|
|
135
|
-
throw new Error('No live Pixifact editor is connected.');
|
|
136
|
-
}
|
|
137
|
-
const id = `live-${Date.now()}-${nextRequestId++}`;
|
|
138
|
-
const message: LiveBridgeRequestMessage = {
|
|
139
|
-
type: 'request',
|
|
140
|
-
id,
|
|
141
|
-
action,
|
|
142
|
-
arguments: args,
|
|
143
|
-
};
|
|
144
|
-
return new Promise((resolve, reject) => {
|
|
145
|
-
const timer = setTimeout(() => {
|
|
146
|
-
pending.delete(id);
|
|
147
|
-
reject(new Error(`Live editor action "${action}" timed out.`));
|
|
148
|
-
}, requestTimeoutMs);
|
|
149
|
-
pending.set(id, {
|
|
150
|
-
resolve,
|
|
151
|
-
reject,
|
|
152
|
-
timer,
|
|
153
|
-
});
|
|
154
|
-
editor!.send(JSON.stringify(message));
|
|
155
|
-
});
|
|
156
|
-
},
|
|
157
|
-
|
|
158
|
-
stop() {
|
|
159
|
-
rejectPending(new Error('Live bridge stopped.'));
|
|
160
|
-
server.stop(true);
|
|
161
|
-
},
|
|
162
|
-
};
|
|
163
|
-
}
|