pixifact-cli 0.5.0 → 0.6.1

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.
@@ -0,0 +1 @@
1
+ import{n as e,r as t,t as n,u as r}from"./index-DoPhYWF1.js";r.add(n,t),r.add(e);
@@ -0,0 +1 @@
1
+ import"./index-DoPhYWF1.js";import"./init-CuKNsO_k.js";
package/editor/index.html CHANGED
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Pixifact Editor</title>
7
- <script type="module" crossorigin src="/assets/index-CAe9Mkmr.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-CVrKIfXK.css">
7
+ <script type="module" crossorigin src="/assets/index-DoPhYWF1.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-D3t5XxiB.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="app"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pixifact-cli",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "Pixifact Scene 自动化 CLI。",
5
5
  "packageManager": "bun@1.3.13",
6
6
  "type": "module",
@@ -33,6 +33,6 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "esbuild": "^0.27.2",
36
- "pixifact": "^0.5.0"
36
+ "pixifact": "^0.6.1"
37
37
  }
38
38
  }
package/src/automation.ts CHANGED
@@ -432,7 +432,7 @@ function validateAllCompilerScenes(root: string) {
432
432
  const failures = results.filter((result): result is CompilerSceneValidationFailure => !result.ok);
433
433
  const scenes = results
434
434
  .filter((result): result is CompilerSceneValidationSuccess => result.ok)
435
- .map(({ scenePath, revision, summary }) => ({ scenePath, revision, summary }));
435
+ .map(({ scenePath, revision }) => ({ scenePath, revision }));
436
436
  if (failures.length > 0) {
437
437
  return {
438
438
  ok: false as const,
@@ -447,7 +447,6 @@ function validateAllCompilerScenes(root: string) {
447
447
  ok: true as const,
448
448
  projectRoot: root,
449
449
  sceneCount: results.length,
450
- scenes,
451
450
  };
452
451
  }
453
452
 
@@ -637,7 +636,7 @@ export function createPixifactAutomation() {
637
636
  const nodeId = assertString(args.node, 'node');
638
637
  const node = collectDetailedCompilerNodes(template.children).find((item) => item.locator === nodeId);
639
638
  if (!node) {
640
- throw new Error(`Node "${nodeId}" was not found.`);
639
+ throw new Error(`Node locator "${nodeId}" was not found.`);
641
640
  }
642
641
  return node;
643
642
  },
@@ -4,9 +4,10 @@ import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { parseSceneTemplate } from 'pixifact/compiler';
6
6
 
7
- export const editorSessionProtocolVersion = 2;
7
+ export const editorSessionProtocolVersion = 3;
8
8
 
9
9
  export type EditorContextSyncState = 'synced' | 'saving' | 'conflict' | 'error';
10
+ export type EditorPreviewState = 'loading' | 'ready' | 'error';
10
11
 
11
12
  export type EditorNodeContext =
12
13
  | {
@@ -41,6 +42,7 @@ export type EditorSelectionContext =
41
42
  export interface EditorBrowserContext {
42
43
  scene: {
43
44
  path: string;
45
+ previewState: EditorPreviewState;
44
46
  revision: string;
45
47
  syncState: EditorContextSyncState;
46
48
  };
@@ -75,6 +77,7 @@ interface EditorBrowserSocket {
75
77
 
76
78
  interface EditorHostSessionOptions {
77
79
  projectRoot: string;
80
+ screenshotTimeoutMs?: number;
78
81
  token: string;
79
82
  now?: () => Date;
80
83
  }
@@ -89,6 +92,25 @@ interface ClaimEditorSessionOptions extends EditorSessionLookupOptions {
89
92
  descriptor: EditorSessionDescriptor;
90
93
  }
91
94
 
95
+ interface PendingEditorScreenshot {
96
+ browser: EditorBrowserSocket;
97
+ resolve(response: Response): void;
98
+ scene: {
99
+ path: string;
100
+ revision: string;
101
+ };
102
+ timeout: ReturnType<typeof setTimeout>;
103
+ }
104
+
105
+ export interface EditorScreenshotResult {
106
+ ok: true;
107
+ scene: string;
108
+ revision: string;
109
+ width: number;
110
+ height: number;
111
+ data: Uint8Array;
112
+ }
113
+
92
114
  function jsonResponse(value: unknown, status = 200) {
93
115
  return Response.json(value, { status });
94
116
  }
@@ -138,6 +160,7 @@ function isEditorBrowserContext(value: unknown): value is EditorBrowserContext {
138
160
  typeof value.scene.path !== 'string'
139
161
  || typeof value.scene.revision !== 'string'
140
162
  || !['synced', 'saving', 'conflict', 'error'].includes(String(syncState))
163
+ || !['loading', 'ready', 'error'].includes(String(value.scene.previewState))
141
164
  ) {
142
165
  return false;
143
166
  }
@@ -152,10 +175,29 @@ function isEditorBrowserContext(value: unknown): value is EditorBrowserContext {
152
175
  export function createEditorHostSession(options: EditorHostSessionOptions) {
153
176
  const projectRoot = fs.realpathSync(options.projectRoot);
154
177
  const now = options.now ?? (() => new Date());
178
+ const screenshotTimeoutMs = options.screenshotTimeoutMs ?? 10_000;
155
179
  const browsers = new Set<EditorBrowserSocket>();
180
+ const pendingScreenshots = new Map<string, PendingEditorScreenshot>();
156
181
  let activeBrowser: EditorBrowserSocket | undefined;
157
182
  let context: EditorContext | undefined;
158
183
  let resume: EditorSessionResumeState | undefined;
184
+ let screenshotSequence = 0;
185
+
186
+ function settleScreenshot(requestId: string, response: Response) {
187
+ const pending = pendingScreenshots.get(requestId);
188
+ if (!pending) return;
189
+ pendingScreenshots.delete(requestId);
190
+ clearTimeout(pending.timeout);
191
+ pending.resolve(response);
192
+ }
193
+
194
+ function rejectBrowserScreenshots(browser: EditorBrowserSocket, error: string) {
195
+ for (const [requestId, pending] of pendingScreenshots) {
196
+ if (pending.browser === browser) {
197
+ settleScreenshot(requestId, jsonResponse({ ok: false, error }, 409));
198
+ }
199
+ }
200
+ }
159
201
 
160
202
  function sendState(
161
203
  socket: EditorBrowserSocket,
@@ -190,6 +232,7 @@ export function createEditorHostSession(options: EditorHostSessionOptions) {
190
232
  function close(socket: EditorBrowserSocket) {
191
233
  browsers.delete(socket);
192
234
  if (socket === activeBrowser) {
235
+ rejectBrowserScreenshots(socket, 'Active Editor browser disconnected during screenshot capture.');
193
236
  activeBrowser = undefined;
194
237
  context = undefined;
195
238
  }
@@ -209,6 +252,9 @@ export function createEditorHostSession(options: EditorHostSessionOptions) {
209
252
  return;
210
253
  }
211
254
  const previous = activeBrowser;
255
+ if (previous) {
256
+ rejectBrowserScreenshots(previous, 'Active Editor browser changed during screenshot capture.');
257
+ }
212
258
  activeBrowser = socket;
213
259
  context = undefined;
214
260
  for (const browser of browsers) {
@@ -222,6 +268,57 @@ export function createEditorHostSession(options: EditorHostSessionOptions) {
222
268
  }
223
269
  return;
224
270
  }
271
+ if (
272
+ socket === activeBrowser
273
+ && (parsed.type === 'editorScreenshotCompleted' || parsed.type === 'editorScreenshotFailed')
274
+ ) {
275
+ const requestId = typeof parsed.requestId === 'string' ? parsed.requestId : '';
276
+ const pending = pendingScreenshots.get(requestId);
277
+ if (!pending || pending.browser !== socket) return;
278
+ if (parsed.type === 'editorScreenshotFailed') {
279
+ const error = typeof parsed.error === 'string'
280
+ ? parsed.error
281
+ : 'Editor browser failed to capture the Scene screenshot.';
282
+ settleScreenshot(requestId, jsonResponse({ ok: false, error }, 500));
283
+ return;
284
+ }
285
+ if (
286
+ !isRecord(parsed.scene)
287
+ || parsed.scene.path !== pending.scene.path
288
+ || parsed.scene.revision !== pending.scene.revision
289
+ || !Number.isInteger(parsed.width)
290
+ || Number(parsed.width) <= 0
291
+ || !Number.isInteger(parsed.height)
292
+ || Number(parsed.height) <= 0
293
+ || typeof parsed.dataUrl !== 'string'
294
+ ) {
295
+ settleScreenshot(requestId, jsonResponse({
296
+ ok: false,
297
+ error: 'Editor browser returned an invalid screenshot response.',
298
+ }, 502));
299
+ return;
300
+ }
301
+ const match = parsed.dataUrl.match(/^data:image\/png;base64,([A-Za-z0-9+/]+={0,2})$/);
302
+ const png = match ? Buffer.from(match[1], 'base64') : undefined;
303
+ if (!png || !png.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) {
304
+ settleScreenshot(requestId, jsonResponse({
305
+ ok: false,
306
+ error: 'Editor browser did not return PNG screenshot data.',
307
+ }, 502));
308
+ return;
309
+ }
310
+ settleScreenshot(requestId, new Response(png, {
311
+ headers: {
312
+ 'cache-control': 'no-store',
313
+ 'content-type': 'image/png',
314
+ 'x-pixifact-height': String(parsed.height),
315
+ 'x-pixifact-revision': pending.scene.revision,
316
+ 'x-pixifact-scene': encodeURIComponent(pending.scene.path),
317
+ 'x-pixifact-width': String(parsed.width),
318
+ },
319
+ }));
320
+ return;
321
+ }
225
322
  if (socket !== activeBrowser || parsed.type !== 'editorContextChanged') return;
226
323
  if (!isEditorBrowserContext(parsed.context)) {
227
324
  throw new Error('Editor context message is invalid.');
@@ -244,7 +341,7 @@ export function createEditorHostSession(options: EditorHostSessionOptions) {
244
341
  notifyStandbyBrowsers();
245
342
  }
246
343
 
247
- function contextResponse() {
344
+ function synchronizedContext() {
248
345
  if (!activeBrowser) {
249
346
  return jsonResponse({
250
347
  ok: false,
@@ -297,12 +394,54 @@ export function createEditorHostSession(options: EditorHostSessionOptions) {
297
394
  },
298
395
  }, 409);
299
396
  }
300
- return jsonResponse(context);
397
+ return context;
398
+ }
399
+
400
+ function contextResponse() {
401
+ const result = synchronizedContext();
402
+ return result instanceof Response ? result : jsonResponse(result);
403
+ }
404
+
405
+ function screenshotResponse() {
406
+ const result = synchronizedContext();
407
+ if (result instanceof Response) return result;
408
+ if (result.scene.previewState !== 'ready') {
409
+ return jsonResponse({
410
+ ok: false,
411
+ error: 'Editor Scene preview is not ready.',
412
+ previewState: result.scene.previewState,
413
+ }, 409);
414
+ }
415
+ const browser = activeBrowser!;
416
+ const requestId = `screenshot-${++screenshotSequence}`;
417
+ const scene = {
418
+ path: result.scene.path,
419
+ revision: result.scene.revision,
420
+ };
421
+ return new Promise<Response>((resolve) => {
422
+ const timeout = setTimeout(() => {
423
+ settleScreenshot(requestId, jsonResponse({
424
+ ok: false,
425
+ error: 'Editor screenshot capture timed out.',
426
+ }, 504));
427
+ }, screenshotTimeoutMs);
428
+ pendingScreenshots.set(requestId, { browser, resolve, scene, timeout });
429
+ browser.send(JSON.stringify({
430
+ type: 'editorScreenshotRequested',
431
+ protocolVersion: editorSessionProtocolVersion,
432
+ requestId,
433
+ scene,
434
+ }));
435
+ });
301
436
  }
302
437
 
303
438
  function fetchRequest(request: Request) {
304
439
  const pathname = new URL(request.url).pathname;
305
- if (pathname !== '/api/editor-session/health' && pathname !== '/api/editor-context') {
440
+ if (
441
+ pathname !== '/api/editor-session/health'
442
+ && pathname !== '/api/editor-context'
443
+ && pathname !== '/api/editor-screenshot'
444
+ ) {
306
445
  return undefined;
307
446
  }
308
447
  if (!authorizationMatches(request, options.token)) {
@@ -314,6 +453,12 @@ export function createEditorHostSession(options: EditorHostSessionOptions) {
314
453
  projectRoot,
315
454
  });
316
455
  }
456
+ if (pathname === '/api/editor-screenshot') {
457
+ if (request.method !== 'POST') {
458
+ return jsonResponse({ ok: false, error: 'Editor screenshot requires POST.' }, 405);
459
+ }
460
+ return screenshotResponse();
461
+ }
317
462
  return contextResponse();
318
463
  }
319
464
 
@@ -488,3 +633,65 @@ export async function queryEditorContext(options: EditorSessionLookupOptions) {
488
633
  };
489
634
  }
490
635
  }
636
+
637
+ export async function captureEditorScreenshot(
638
+ options: EditorSessionLookupOptions,
639
+ ): Promise<EditorScreenshotResult | { ok: false; error: string; [key: string]: unknown }> {
640
+ const projectRoot = fs.realpathSync(options.projectRoot);
641
+ const descriptor = readEditorSessionDescriptor(projectRoot, options.sessionsRoot);
642
+ if (!descriptor) {
643
+ return {
644
+ ok: false,
645
+ error: 'No Pixifact Editor Host is running for this project.',
646
+ };
647
+ }
648
+ if (descriptor.projectRoot !== projectRoot) {
649
+ return {
650
+ ok: false,
651
+ error: 'Pixifact Editor session belongs to a different project.',
652
+ };
653
+ }
654
+ const fetcher = options.fetch ?? fetch;
655
+ let response: Response;
656
+ try {
657
+ response = await fetcher(`${descriptor.origin}/api/editor-screenshot`, {
658
+ method: 'POST',
659
+ headers: { authorization: `Bearer ${descriptor.token}` },
660
+ });
661
+ } catch {
662
+ return {
663
+ ok: false,
664
+ error: 'Pixifact Editor Host is not reachable for this project.',
665
+ };
666
+ }
667
+ if (!response.ok) {
668
+ return await response.json() as { ok: false; error: string; [key: string]: unknown };
669
+ }
670
+ const sceneHeader = response.headers.get('x-pixifact-scene');
671
+ const revision = response.headers.get('x-pixifact-revision');
672
+ const width = Number(response.headers.get('x-pixifact-width'));
673
+ const height = Number(response.headers.get('x-pixifact-height'));
674
+ if (
675
+ response.headers.get('content-type') !== 'image/png'
676
+ || !sceneHeader
677
+ || !revision
678
+ || !Number.isInteger(width)
679
+ || width <= 0
680
+ || !Number.isInteger(height)
681
+ || height <= 0
682
+ ) {
683
+ return { ok: false, error: 'Pixifact Editor Host returned invalid screenshot metadata.' };
684
+ }
685
+ const data = new Uint8Array(await response.arrayBuffer());
686
+ if (!Buffer.from(data.subarray(0, 8)).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) {
687
+ return { ok: false, error: 'Pixifact Editor Host did not return PNG screenshot data.' };
688
+ }
689
+ return {
690
+ ok: true,
691
+ scene: decodeURIComponent(sceneHeader),
692
+ revision,
693
+ width,
694
+ height,
695
+ data,
696
+ };
697
+ }
@@ -1,10 +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
8
  import { startEditorServer } from './editorServer';
7
- import { queryEditorContext } from './editorSession';
9
+ import { captureEditorScreenshot, queryEditorContext } from './editorSession';
10
+ import { queryRuntime, queryRuntimeList } from './runtimeSession';
11
+ import type { RuntimeLogLevel, RuntimeRequest } from 'pixifact/runtime-dev';
8
12
  import {
9
13
  buildWechatTarget,
10
14
  devWechatTarget,
@@ -17,8 +21,11 @@ type Automation = ReturnType<typeof createPixifactAutomation>;
17
21
 
18
22
  interface CliOptions {
19
23
  automation?: Automation;
24
+ captureEditorScreenshot?: typeof captureEditorScreenshot;
20
25
  onWechatDevEvent?: (event: WechatDevEvent) => void;
21
26
  readEditorContext?: typeof queryEditorContext;
27
+ listRuntimes?: typeof queryRuntimeList;
28
+ queryRuntime?: typeof queryRuntime;
22
29
  startEditor?: typeof startEditorServer;
23
30
  }
24
31
 
@@ -82,6 +89,23 @@ function projectRootFlag(flags: Record<string, string | true>) {
82
89
  return requireFlag(flags, 'project-root');
83
90
  }
84
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
+
85
109
  function jsonLine(value: unknown) {
86
110
  return `${JSON.stringify(value, null, 2)}\n`;
87
111
  }
@@ -92,6 +116,13 @@ function isFailedResult(value: unknown): value is CliJsonResult {
92
116
  && (value as CliJsonResult).ok === false;
93
117
  }
94
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
+
95
126
  function compileScenesFailure(error: unknown): CliJsonResult {
96
127
  const message = error instanceof Error ? error.message : String(error);
97
128
  if (error instanceof CompileSceneError) {
@@ -228,10 +259,88 @@ async function executeFileCommand(
228
259
  onWechatDevEvent?: (event: WechatDevEvent) => void,
229
260
  startEditor: typeof startEditorServer = startEditorServer,
230
261
  readEditorContext: typeof queryEditorContext = queryEditorContext,
262
+ captureScreenshot: typeof captureEditorScreenshot = captureEditorScreenshot,
263
+ listRuntimes: typeof queryRuntimeList = queryRuntimeList,
264
+ sendRuntimeRequest: typeof queryRuntime = queryRuntime,
231
265
  ) {
232
266
  const [area, action] = positionals;
233
267
  const projectRoot = projectRootFlag(flags);
234
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
+
235
344
  if (area === 'editor' && action === undefined) {
236
345
  const session = await startEditor({ projectRoot });
237
346
  return {
@@ -245,6 +354,22 @@ async function executeFileCommand(
245
354
  return readEditorContext({ projectRoot });
246
355
  }
247
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
+
248
373
  if ((area === 'build' || area === 'dev' || area === 'validate') && action === undefined) {
249
374
  const target = requireFlag(flags, 'target');
250
375
  if (target !== 'wechat') {
@@ -392,12 +517,26 @@ export async function executePixifactCli(argv: string[], options: CliOptions = {
392
517
  aiValidationAlternatives: [
393
518
  'scene validate --all',
394
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
+ ],
395
532
  auxiliaryCommands: [
396
533
  'editor',
397
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
  ],
539
+ nodeLocatorSource: 'scene inspect --scene <scene-path> returns node locator values',
401
540
  defaults: {
402
541
  projectRoot: 'current working directory',
403
542
  },
@@ -414,6 +553,9 @@ export async function executePixifactCli(argv: string[], options: CliOptions = {
414
553
  options.onWechatDevEvent,
415
554
  options.startEditor,
416
555
  options.readEditorContext,
556
+ options.captureEditorScreenshot,
557
+ options.listRuntimes,
558
+ options.queryRuntime,
417
559
  );
418
560
  if (isFailedResult(result)) {
419
561
  return {