pixifact-cli 0.7.0 → 0.9.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.
@@ -0,0 +1 @@
1
+ import{n as e,r as t,t as n,u as r}from"./index-BUZ_WtnT.js";r.add(n,t),r.add(e);
@@ -0,0 +1 @@
1
+ import"./index-BUZ_WtnT.js";import"./init-CzHI7RKj.js";
package/editor/index.html CHANGED
@@ -4,7 +4,7 @@
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-DoPhYWF1.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-BUZ_WtnT.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/assets/index-D3t5XxiB.css">
9
9
  </head>
10
10
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pixifact-cli",
3
- "version": "0.7.0",
3
+ "version": "0.9.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.9.0",
36
36
  "vite": "^8.0.10"
37
37
  }
38
38
  }
package/src/automation.ts CHANGED
@@ -33,7 +33,6 @@ interface ToolInput {
33
33
  projectRoot?: unknown;
34
34
  scenePath?: unknown;
35
35
  node?: unknown;
36
- name?: unknown;
37
36
  }
38
37
 
39
38
  type CompilerSceneValidationSuccess = ReturnType<typeof validateCompilerSceneFile> & { ok: true };
@@ -576,8 +575,8 @@ export function createPixifactAutomation() {
576
575
  createScene(input: unknown) {
577
576
  const args = assertRecord(input, 'input') as ToolInput;
578
577
  const { root, target } = resolveProjectPath(args.projectRoot, args.scenePath);
579
- const sceneName = assertString(args.name, 'name');
580
578
  const scenePath = normalizeSceneAssetId(path.relative(root, target));
579
+ const sceneName = sceneLocalName(scenePath);
581
580
  const sourceRootFailure = compilerSceneSourceRootFailure(scenePath);
582
581
  if (sourceRootFailure) {
583
582
  return sourceRootFailure;
@@ -36,6 +36,10 @@ interface EditorProjectFile {
36
36
  path: string;
37
37
  }
38
38
 
39
+ interface EditorUiState {
40
+ assetTreeExpandedDirectories?: string[];
41
+ }
42
+
39
43
  interface BunServer {
40
44
  hostname: string;
41
45
  port: number;
@@ -163,6 +167,35 @@ function requestPath(url: URL) {
163
167
  return url.searchParams.get('path') ?? '';
164
168
  }
165
169
 
170
+ function editorUiStatePath(projectRoot: string) {
171
+ return path.join(projectRoot, '.pixifact', 'editor', 'ui-state.json');
172
+ }
173
+
174
+ function parseEditorUiState(value: unknown): EditorUiState {
175
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
176
+ throw new Error('Editor UI state must be a JSON object.');
177
+ }
178
+ const directories = (value as Record<string, unknown>).assetTreeExpandedDirectories;
179
+ if (directories === undefined) return {};
180
+ if (!Array.isArray(directories) || directories.some((directory) => typeof directory !== 'string')) {
181
+ throw new Error('Editor UI state assetTreeExpandedDirectories must be a string array.');
182
+ }
183
+ return { assetTreeExpandedDirectories: directories };
184
+ }
185
+
186
+ function readEditorUiState(projectRoot: string): EditorUiState {
187
+ const statePath = editorUiStatePath(projectRoot);
188
+ if (!fs.existsSync(statePath)) return {};
189
+ return parseEditorUiState(JSON.parse(fs.readFileSync(statePath, 'utf8')));
190
+ }
191
+
192
+ function writeEditorUiState(projectRoot: string, state: EditorUiState) {
193
+ const statePath = editorUiStatePath(projectRoot);
194
+ fs.mkdirSync(path.dirname(statePath), { recursive: true });
195
+ fs.writeFileSync(statePath, JSON.stringify(state, null, 2) + '\n', 'utf8');
196
+ return state;
197
+ }
198
+
166
199
  async function readJsonBody(request: Request) {
167
200
  const value = await request.json();
168
201
  if (!value || typeof value !== 'object') {
@@ -205,6 +238,17 @@ export function createEditorProjectService(options: EditorProjectServiceOptions)
205
238
  images: files.filter((file) => file.kind === 'image').map((file) => file.path),
206
239
  });
207
240
  }
241
+ if (url.pathname === '/api/editor-ui-state' && request.method === 'GET') {
242
+ return jsonResponse(readEditorUiState(projectRoot));
243
+ }
244
+ if (url.pathname === '/api/editor-ui-state' && request.method === 'PUT') {
245
+ const body = await readJsonBody(request);
246
+ const state = parseEditorUiState(body);
247
+ if (!state.assetTreeExpandedDirectories) {
248
+ throw new EditorRequestError(400, 'Editor UI state requires assetTreeExpandedDirectories.');
249
+ }
250
+ return jsonResponse(writeEditorUiState(projectRoot, state));
251
+ }
208
252
  if (url.pathname === '/api/scene' && request.method === 'GET') {
209
253
  const scenePath = requestPath(url);
210
254
  if (path.extname(scenePath).toLowerCase() !== '.scene') {
@@ -275,6 +319,9 @@ export function watchEditorProject(projectRoot: string, onProjectFileChanged: (p
275
319
  const watcher = fs.watch(projectRoot, { recursive: true }, (_event, fileName) => {
276
320
  if (!fileName) return;
277
321
  const filePath = String(fileName);
322
+ if (filePath.split(/[\\/]/).some((segment) => ignoredDirectories.has(segment))) {
323
+ return;
324
+ }
278
325
  const absolutePath = path.resolve(projectRoot, filePath);
279
326
  if (fs.existsSync(absolutePath) && fs.statSync(absolutePath).isDirectory()) {
280
327
  return;
@@ -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;
@@ -463,10 +491,17 @@ async function executeFileCommand(
463
491
  }
464
492
 
465
493
  if (area === 'scene' && action === 'create') {
494
+ assertAllowedFlags(flags, ['project-root'], 'scene create');
495
+ const scenePath = positionals[2];
496
+ if (!scenePath) {
497
+ throw new Error('Scene create requires a project-relative .scene path.');
498
+ }
499
+ if (positionals[3] !== undefined) {
500
+ throw new Error('Scene create accepts exactly one scene path.');
501
+ }
466
502
  return automation.createScene({
467
503
  projectRoot,
468
- scenePath: requireFlag(flags, 'scene'),
469
- name: requireFlag(flags, 'name'),
504
+ scenePath,
470
505
  });
471
506
  }
472
507
 
@@ -525,6 +560,7 @@ export async function executePixifactCli(argv: string[], options: CliOptions = {
525
560
  runtimeCommands: [
526
561
  'runtime list',
527
562
  'runtime tree [--output <json-path>] [--runtime <runtime-id>]',
563
+ 'runtime screenshot [--output <png-path>] [--runtime <runtime-id>]',
528
564
  'runtime node <pixi-uid> [--runtime <runtime-id>]',
529
565
  'runtime state [--runtime <runtime-id>]',
530
566
  'runtime logs [--after <seq>] [--level <level>] [--runtime <runtime-id>]',
@@ -538,12 +574,13 @@ export async function executePixifactCli(argv: string[], options: CliOptions = {
538
574
  'editor',
539
575
  'editor context',
540
576
  'editor screenshot --output <png-path>',
541
- 'scene create --scene <scene-path> --name <SceneName>',
577
+ 'scene create <scene-path>',
542
578
  'node inspect --scene <scene-path> --node <locator>',
543
579
  ],
544
580
  nodeLocatorSource: 'scene inspect --scene <scene-path> returns node locator values',
545
581
  defaults: {
546
582
  projectRoot: 'current working directory',
583
+ runtimeScreenshotOutput: runtimeScreenshotDefaultOutput,
547
584
  },
548
585
  }),
549
586
  stderr: '',
@@ -561,6 +598,7 @@ export async function executePixifactCli(argv: string[], options: CliOptions = {
561
598
  options.captureEditorScreenshot,
562
599
  options.listRuntimes,
563
600
  options.queryRuntime,
601
+ options.captureRuntimeScreenshot,
564
602
  );
565
603
  if (isFailedResult(result)) {
566
604
  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),
@@ -1 +0,0 @@
1
- import{a as e,c as t,i as n,l as r,o as i,s as a,u as o}from"./index-DoPhYWF1.js";import"./init-CuKNsO_k.js";o.add(t),o.mixin(r,a),o.add(i),o.add(e),o.mixin(r,n);