pixifact-cli 0.7.0 → 0.8.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 CHANGED
@@ -38,6 +38,7 @@ pixifact scene validate --all
38
38
  ```bash
39
39
  pixifact runtime list
40
40
  pixifact runtime tree --output .pixifact/runtime/tree.json
41
+ pixifact runtime screenshot
41
42
  pixifact runtime node <pixi-uid>
42
43
  pixifact runtime state
43
44
  pixifact runtime logs --after <seq>
@@ -49,6 +50,8 @@ pixifact runtime input key Space
49
50
 
50
51
  `runtime tree` 默认将节点树输出到终端;传入 `--output <json-path>` 时,改为保存运行时快照 JSON,终端只返回路径和采集元数据。快照包含 `schemaVersion`、`capturedAt`、当前 `runtimeId` 和 `root` 节点树,适合 Agent 使用文本搜索或 JSON 工具分析。快照中的 `runtimeId` 和 PixiJS `uid` 只对本次运行有效,不应写回项目源文件。
51
52
 
53
+ `runtime screenshot` 默认将当前 PixiJS Canvas 的 PNG 截图写入项目根下的 `.pixifact/runtime/frame.png`,也可用 `--output <png-path>` 指定路径。截图使用当前 `app.screen` 逻辑尺寸和 renderer 背景色,只包含 `app.stage` 的 PixiJS 渲染内容,不包含浏览器 UI、HTML/CSS 或 DOM overlay;多个页面时添加 `--runtime <runtime-id>`。
54
+
52
55
  ## 浏览器 Editor
53
56
 
54
57
  在目标项目根目录运行:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pixifact-cli",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Pixifact Scene 自动化 CLI。",
5
5
  "packageManager": "bun@1.3.13",
6
6
  "type": "module",
@@ -32,7 +32,7 @@
32
32
  "start": "bun src/pixifact-cli.ts"
33
33
  },
34
34
  "dependencies": {
35
- "pixifact": "^0.7.0",
35
+ "pixifact": "^0.8.0",
36
36
  "vite": "^8.0.10"
37
37
  }
38
38
  }
@@ -8,7 +8,7 @@ import { CompileSceneError, compileScenes } from 'pixifact/compiler-node';
8
8
  import type { SceneValidationDiagnostic } from 'pixifact/compiler';
9
9
  import { startEditorServer } from './editorServer';
10
10
  import { captureEditorScreenshot, queryEditorContext } from './editorSession';
11
- import { queryRuntime, queryRuntimeList } from './runtimeSession';
11
+ import { captureRuntimeScreenshot, queryRuntime, queryRuntimeList } from './runtimeSession';
12
12
  import type { RuntimeLogLevel, RuntimeRequest } from 'pixifact/runtime-dev';
13
13
  import {
14
14
  buildPixifactTarget,
@@ -23,6 +23,7 @@ type Automation = ReturnType<typeof createPixifactAutomation>;
23
23
  interface CliOptions {
24
24
  automation?: Automation;
25
25
  captureEditorScreenshot?: typeof captureEditorScreenshot;
26
+ captureRuntimeScreenshot?: typeof captureRuntimeScreenshot;
26
27
  onDevEvent?: (event: PixifactDevEvent) => void;
27
28
  readEditorContext?: typeof queryEditorContext;
28
29
  listRuntimes?: typeof queryRuntimeList;
@@ -46,6 +47,8 @@ interface ParsedArgs {
46
47
  flags: Record<string, string | true>;
47
48
  }
48
49
 
50
+ const runtimeScreenshotDefaultOutput = '.pixifact/runtime/frame.png';
51
+
49
52
  function parseArgs(argv: string[]): ParsedArgs {
50
53
  const positionals: string[] = [];
51
54
  const flags: Record<string, string | true> = {};
@@ -274,6 +277,7 @@ async function executeFileCommand(
274
277
  captureScreenshot: typeof captureEditorScreenshot = captureEditorScreenshot,
275
278
  listRuntimes: typeof queryRuntimeList = queryRuntimeList,
276
279
  sendRuntimeRequest: typeof queryRuntime = queryRuntime,
280
+ captureRuntime: typeof captureRuntimeScreenshot = captureRuntimeScreenshot,
277
281
  ) {
278
282
  const [area, action] = positionals;
279
283
  const projectRoot = projectRootFlag(flags);
@@ -282,6 +286,30 @@ async function executeFileCommand(
282
286
  return listRuntimes({ projectRoot });
283
287
  }
284
288
 
289
+ if (area === 'runtime' && action === 'screenshot') {
290
+ assertAllowedFlags(flags, ['output', 'project-root', 'runtime'], 'runtime screenshot');
291
+ const output = flags.output === undefined
292
+ ? path.resolve(projectRoot, runtimeScreenshotDefaultOutput)
293
+ : path.resolve(requireFlag(flags, 'output'));
294
+ const screenshot = await captureRuntime({
295
+ projectRoot,
296
+ runtimeId: optionalFlag(flags, 'runtime'),
297
+ });
298
+ if (!screenshot.ok) return screenshot;
299
+ const capturedAt = new Date().toISOString();
300
+ fs.mkdirSync(path.dirname(output), { recursive: true });
301
+ fs.writeFileSync(output, screenshot.data);
302
+ return {
303
+ ok: true,
304
+ runtimeId: screenshot.runtimeId,
305
+ width: screenshot.width,
306
+ height: screenshot.height,
307
+ bytes: screenshot.data.byteLength,
308
+ output,
309
+ capturedAt,
310
+ };
311
+ }
312
+
285
313
  if (area === 'runtime' && action !== undefined) {
286
314
  const runtimeId = optionalFlag(flags, 'runtime');
287
315
  let request: RuntimeRequest;
@@ -525,6 +553,7 @@ export async function executePixifactCli(argv: string[], options: CliOptions = {
525
553
  runtimeCommands: [
526
554
  'runtime list',
527
555
  'runtime tree [--output <json-path>] [--runtime <runtime-id>]',
556
+ 'runtime screenshot [--output <png-path>] [--runtime <runtime-id>]',
528
557
  'runtime node <pixi-uid> [--runtime <runtime-id>]',
529
558
  'runtime state [--runtime <runtime-id>]',
530
559
  'runtime logs [--after <seq>] [--level <level>] [--runtime <runtime-id>]',
@@ -544,6 +573,7 @@ export async function executePixifactCli(argv: string[], options: CliOptions = {
544
573
  nodeLocatorSource: 'scene inspect --scene <scene-path> returns node locator values',
545
574
  defaults: {
546
575
  projectRoot: 'current working directory',
576
+ runtimeScreenshotOutput: runtimeScreenshotDefaultOutput,
547
577
  },
548
578
  }),
549
579
  stderr: '',
@@ -561,6 +591,7 @@ export async function executePixifactCli(argv: string[], options: CliOptions = {
561
591
  options.captureEditorScreenshot,
562
592
  options.listRuntimes,
563
593
  options.queryRuntime,
594
+ options.captureRuntimeScreenshot,
564
595
  );
565
596
  if (isFailedResult(result)) {
566
597
  return {
@@ -4,6 +4,8 @@ import {
4
4
  } from 'pixifact/compiler-node';
5
5
  import type { RuntimePageDescriptor, RuntimeRequest } from 'pixifact/runtime-dev';
6
6
 
7
+ const pngSignature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
8
+
7
9
  interface RuntimeSessionLookupOptions {
8
10
  projectRoot: string;
9
11
  sessionsRoot?: string;
@@ -131,3 +133,61 @@ export async function queryRuntime(options: RuntimeQueryOptions) {
131
133
  }),
132
134
  });
133
135
  }
136
+
137
+ export async function captureRuntimeScreenshot(
138
+ options: RuntimeSessionLookupOptions & { runtimeId?: string },
139
+ ): Promise<
140
+ | { ok: true; runtimeId: string; width: number; height: number; data: Uint8Array }
141
+ | { ok: false; error: string; [key: string]: unknown }
142
+ > {
143
+ const active = await activeDescriptor(options);
144
+ if (!active.ok) return active;
145
+ const fetcher = options.fetch ?? fetch;
146
+ const listed = await runtimeHostFetch(active.descriptor, 'list', fetcher);
147
+ if (listed.ok !== true || !Array.isArray(listed.runtimes)) return listed;
148
+ const selected = selectRuntime(
149
+ listed.runtimes as Array<RuntimePageDescriptor & { connectedAt: string }>,
150
+ options.runtimeId,
151
+ );
152
+ if (!selected.ok) return selected;
153
+
154
+ let response: Response;
155
+ try {
156
+ response = await fetcher(`${active.descriptor.origin}/__pixifact_runtime__/screenshot`, {
157
+ method: 'POST',
158
+ headers: {
159
+ authorization: `Bearer ${active.descriptor.token}`,
160
+ 'content-type': 'application/json',
161
+ },
162
+ body: JSON.stringify({ runtimeId: selected.runtime.runtimeId }),
163
+ });
164
+ } catch {
165
+ return {
166
+ ok: false,
167
+ error: 'Pixifact Runtime Vite server is not reachable for this project.',
168
+ };
169
+ }
170
+ if (!response.ok) {
171
+ return await response.json() as { ok: false; error: string; [key: string]: unknown };
172
+ }
173
+
174
+ const runtimeId = response.headers.get('x-pixifact-runtime');
175
+ const width = Number(response.headers.get('x-pixifact-width'));
176
+ const height = Number(response.headers.get('x-pixifact-height'));
177
+ if (
178
+ response.headers.get('content-type') !== 'image/png'
179
+ || runtimeId !== selected.runtime.runtimeId
180
+ || !Number.isInteger(width)
181
+ || width <= 0
182
+ || !Number.isInteger(height)
183
+ || height <= 0
184
+ ) {
185
+ return { ok: false, error: 'Pixifact Runtime Host returned invalid screenshot metadata.' };
186
+ }
187
+
188
+ const data = new Uint8Array(await response.arrayBuffer());
189
+ if (!Buffer.from(data.subarray(0, pngSignature.length)).equals(pngSignature)) {
190
+ return { ok: false, error: 'Pixifact Runtime Host did not return PNG screenshot data.' };
191
+ }
192
+ return { ok: true, runtimeId, width, height, data };
193
+ }
package/src/viteTarget.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { access, readFile, readdir, stat } from 'node:fs/promises';
1
+ import { access, readFile, readdir, realpath, stat } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import {
4
4
  build,
@@ -55,7 +55,7 @@ export class PixifactTargetError extends Error {
55
55
  }
56
56
 
57
57
  export async function validatePixifactTarget(projectRootInput: string, mode: string) {
58
- const projectRoot = path.resolve(projectRootInput);
58
+ const projectRoot = await realpath(path.resolve(projectRootInput));
59
59
  const { platform } = loadPixifactEnv(projectRoot, mode);
60
60
  const config = parsePixifactProjectConfig(JSON.parse(await readFile(
61
61
  path.join(projectRoot, pixifactProjectConfigFileName),