pixifact-cli 0.3.0 → 0.5.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 +32 -1
- package/editor/assets/browserAll-CdyZj0BN.js +1 -0
- package/editor/assets/index-CAe9Mkmr.js +1130 -0
- package/editor/assets/index-CVrKIfXK.css +1 -0
- package/editor/assets/init-C32NAhsC.js +1 -0
- package/editor/assets/webworkerAll-BEmxAAYf.js +1 -0
- package/editor/index.html +13 -0
- package/package.json +5 -3
- package/src/automation.ts +16 -7
- package/src/editorServer.ts +375 -0
- package/src/editorSession.ts +490 -0
- package/src/pixifact-cli.ts +127 -47
- package/src/wechatTarget.ts +578 -0
- package/src/liveBridge.ts +0 -28
- package/src/liveBridgeServer.ts +0 -163
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { parseSceneTemplate } from 'pixifact/compiler';
|
|
6
|
+
|
|
7
|
+
export const editorSessionProtocolVersion = 2;
|
|
8
|
+
|
|
9
|
+
export type EditorContextSyncState = 'synced' | 'saving' | 'conflict' | 'error';
|
|
10
|
+
|
|
11
|
+
export type EditorNodeContext =
|
|
12
|
+
| {
|
|
13
|
+
kind: 'pixi';
|
|
14
|
+
type: string;
|
|
15
|
+
id?: string;
|
|
16
|
+
props: Record<string, unknown>;
|
|
17
|
+
childCount: number;
|
|
18
|
+
}
|
|
19
|
+
| {
|
|
20
|
+
kind: 'sceneInstance';
|
|
21
|
+
type: string;
|
|
22
|
+
id?: string;
|
|
23
|
+
scene: string;
|
|
24
|
+
props: Record<string, unknown>;
|
|
25
|
+
events: Record<string, string>;
|
|
26
|
+
slots: Record<string, number>;
|
|
27
|
+
}
|
|
28
|
+
| {
|
|
29
|
+
kind: 'slotOutlet';
|
|
30
|
+
name: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type EditorSelectionContext =
|
|
34
|
+
| { kind: 'scene' }
|
|
35
|
+
| {
|
|
36
|
+
kind: 'node';
|
|
37
|
+
locator: string;
|
|
38
|
+
node: EditorNodeContext;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export interface EditorBrowserContext {
|
|
42
|
+
scene: {
|
|
43
|
+
path: string;
|
|
44
|
+
revision: string;
|
|
45
|
+
syncState: EditorContextSyncState;
|
|
46
|
+
};
|
|
47
|
+
selection: EditorSelectionContext;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface EditorSessionResumeState {
|
|
51
|
+
scenePath: string;
|
|
52
|
+
selectedLocator?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface EditorContext extends EditorBrowserContext {
|
|
56
|
+
protocolVersion: typeof editorSessionProtocolVersion;
|
|
57
|
+
projectRoot: string;
|
|
58
|
+
editor: {
|
|
59
|
+
connected: true;
|
|
60
|
+
updatedAt: string;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface EditorSessionDescriptor {
|
|
65
|
+
protocolVersion: typeof editorSessionProtocolVersion;
|
|
66
|
+
projectRoot: string;
|
|
67
|
+
pid: number;
|
|
68
|
+
origin: string;
|
|
69
|
+
token: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface EditorBrowserSocket {
|
|
73
|
+
send(data: string): void;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface EditorHostSessionOptions {
|
|
77
|
+
projectRoot: string;
|
|
78
|
+
token: string;
|
|
79
|
+
now?: () => Date;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface EditorSessionLookupOptions {
|
|
83
|
+
projectRoot: string;
|
|
84
|
+
sessionsRoot?: string;
|
|
85
|
+
fetch?: typeof fetch;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface ClaimEditorSessionOptions extends EditorSessionLookupOptions {
|
|
89
|
+
descriptor: EditorSessionDescriptor;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function jsonResponse(value: unknown, status = 200) {
|
|
93
|
+
return Response.json(value, { status });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function sceneVersion(source: string) {
|
|
97
|
+
return `sha256:${createHash('sha256').update(source).digest('hex')}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function authorizationMatches(request: Request, token: string) {
|
|
101
|
+
return request.headers.get('authorization') === `Bearer ${token}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function resolveScenePath(projectRoot: string, scenePath: string) {
|
|
105
|
+
if (!scenePath || path.isAbsolute(scenePath) || path.extname(scenePath).toLowerCase() !== '.scene') {
|
|
106
|
+
throw new Error('Editor context Scene path must be a project-relative .scene file.');
|
|
107
|
+
}
|
|
108
|
+
const absolutePath = fs.realpathSync(path.resolve(projectRoot, scenePath));
|
|
109
|
+
if (absolutePath !== projectRoot && !absolutePath.startsWith(`${projectRoot}${path.sep}`)) {
|
|
110
|
+
throw new Error('Editor context Scene path must stay inside the project root.');
|
|
111
|
+
}
|
|
112
|
+
return absolutePath;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function sourcePosition(message: string, source: string) {
|
|
116
|
+
const match = message.match(/\boffset (\d+)\b/);
|
|
117
|
+
if (!match) {
|
|
118
|
+
return {};
|
|
119
|
+
}
|
|
120
|
+
const offset = Number(match[1]);
|
|
121
|
+
const lines = source.slice(0, offset).split('\n');
|
|
122
|
+
return {
|
|
123
|
+
line: lines.length,
|
|
124
|
+
column: lines.at(-1)!.length + 1,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
129
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isEditorBrowserContext(value: unknown): value is EditorBrowserContext {
|
|
133
|
+
if (!isRecord(value) || !isRecord(value.scene) || !isRecord(value.selection)) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
const syncState = value.scene.syncState;
|
|
137
|
+
if (
|
|
138
|
+
typeof value.scene.path !== 'string'
|
|
139
|
+
|| typeof value.scene.revision !== 'string'
|
|
140
|
+
|| !['synced', 'saving', 'conflict', 'error'].includes(String(syncState))
|
|
141
|
+
) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
if (value.selection.kind === 'scene') {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
return value.selection.kind === 'node'
|
|
148
|
+
&& typeof value.selection.locator === 'string'
|
|
149
|
+
&& isRecord(value.selection.node);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function createEditorHostSession(options: EditorHostSessionOptions) {
|
|
153
|
+
const projectRoot = fs.realpathSync(options.projectRoot);
|
|
154
|
+
const now = options.now ?? (() => new Date());
|
|
155
|
+
const browsers = new Set<EditorBrowserSocket>();
|
|
156
|
+
let activeBrowser: EditorBrowserSocket | undefined;
|
|
157
|
+
let context: EditorContext | undefined;
|
|
158
|
+
let resume: EditorSessionResumeState | undefined;
|
|
159
|
+
|
|
160
|
+
function sendState(
|
|
161
|
+
socket: EditorBrowserSocket,
|
|
162
|
+
type: 'editorSessionActive' | 'editorSessionStandby',
|
|
163
|
+
details: { error?: string; reason?: 'takenOver' } = {},
|
|
164
|
+
) {
|
|
165
|
+
socket.send(JSON.stringify({
|
|
166
|
+
type,
|
|
167
|
+
protocolVersion: editorSessionProtocolVersion,
|
|
168
|
+
...details,
|
|
169
|
+
...(resume ? { resume } : {}),
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function notifyStandbyBrowsers() {
|
|
174
|
+
for (const socket of browsers) {
|
|
175
|
+
if (socket !== activeBrowser) sendState(socket, 'editorSessionStandby');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function open(socket: EditorBrowserSocket) {
|
|
180
|
+
browsers.add(socket);
|
|
181
|
+
if (activeBrowser) {
|
|
182
|
+
sendState(socket, 'editorSessionStandby');
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
activeBrowser = socket;
|
|
186
|
+
sendState(socket, 'editorSessionActive');
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function close(socket: EditorBrowserSocket) {
|
|
191
|
+
browsers.delete(socket);
|
|
192
|
+
if (socket === activeBrowser) {
|
|
193
|
+
activeBrowser = undefined;
|
|
194
|
+
context = undefined;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function message(socket: EditorBrowserSocket, data: string | ArrayBuffer | Uint8Array) {
|
|
199
|
+
const parsed = JSON.parse(String(data)) as Record<string, unknown>;
|
|
200
|
+
if (parsed.protocolVersion !== editorSessionProtocolVersion) {
|
|
201
|
+
throw new Error('Editor session protocol version does not match. Restart Pixifact Editor.');
|
|
202
|
+
}
|
|
203
|
+
if (parsed.type === 'editorSessionTakeoverRequested') {
|
|
204
|
+
if (!browsers.has(socket) || socket === activeBrowser) return;
|
|
205
|
+
if (activeBrowser && context?.scene.syncState !== 'synced') {
|
|
206
|
+
sendState(socket, 'editorSessionStandby', {
|
|
207
|
+
error: '当前 Editor 尚未同步,暂时无法接管。',
|
|
208
|
+
});
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const previous = activeBrowser;
|
|
212
|
+
activeBrowser = socket;
|
|
213
|
+
context = undefined;
|
|
214
|
+
for (const browser of browsers) {
|
|
215
|
+
if (browser === socket) {
|
|
216
|
+
sendState(browser, 'editorSessionActive');
|
|
217
|
+
} else {
|
|
218
|
+
sendState(browser, 'editorSessionStandby', {
|
|
219
|
+
...(browser === previous ? { reason: 'takenOver' as const } : {}),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (socket !== activeBrowser || parsed.type !== 'editorContextChanged') return;
|
|
226
|
+
if (!isEditorBrowserContext(parsed.context)) {
|
|
227
|
+
throw new Error('Editor context message is invalid.');
|
|
228
|
+
}
|
|
229
|
+
context = {
|
|
230
|
+
protocolVersion: editorSessionProtocolVersion,
|
|
231
|
+
projectRoot,
|
|
232
|
+
editor: {
|
|
233
|
+
connected: true,
|
|
234
|
+
updatedAt: now().toISOString(),
|
|
235
|
+
},
|
|
236
|
+
...structuredClone(parsed.context),
|
|
237
|
+
};
|
|
238
|
+
resume = {
|
|
239
|
+
scenePath: context.scene.path,
|
|
240
|
+
...(context.selection.kind === 'node'
|
|
241
|
+
? { selectedLocator: context.selection.locator }
|
|
242
|
+
: {}),
|
|
243
|
+
};
|
|
244
|
+
notifyStandbyBrowsers();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function contextResponse() {
|
|
248
|
+
if (!activeBrowser) {
|
|
249
|
+
return jsonResponse({
|
|
250
|
+
ok: false,
|
|
251
|
+
error: 'No active Editor browser session.',
|
|
252
|
+
}, 409);
|
|
253
|
+
}
|
|
254
|
+
if (!context) {
|
|
255
|
+
return jsonResponse({
|
|
256
|
+
ok: false,
|
|
257
|
+
error: 'Editor context is not ready.',
|
|
258
|
+
}, 409);
|
|
259
|
+
}
|
|
260
|
+
if (context.scene.syncState !== 'synced') {
|
|
261
|
+
return jsonResponse({
|
|
262
|
+
ok: false,
|
|
263
|
+
error: 'Editor Scene is not synchronized.',
|
|
264
|
+
syncState: context.scene.syncState,
|
|
265
|
+
}, 409);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const source = fs.readFileSync(resolveScenePath(projectRoot, context.scene.path), 'utf8');
|
|
269
|
+
const revision = sceneVersion(source);
|
|
270
|
+
if (revision !== context.scene.revision) {
|
|
271
|
+
try {
|
|
272
|
+
parseSceneTemplate(source);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
275
|
+
return jsonResponse({
|
|
276
|
+
ok: false,
|
|
277
|
+
error: 'Scene parsing failed.',
|
|
278
|
+
scene: {
|
|
279
|
+
path: context.scene.path,
|
|
280
|
+
revision,
|
|
281
|
+
},
|
|
282
|
+
diagnostics: [{
|
|
283
|
+
path: '__scene__',
|
|
284
|
+
prop: 'source',
|
|
285
|
+
expected: 'valid Pixifact .scene source',
|
|
286
|
+
actual: message,
|
|
287
|
+
...sourcePosition(message, source),
|
|
288
|
+
}],
|
|
289
|
+
}, 422);
|
|
290
|
+
}
|
|
291
|
+
return jsonResponse({
|
|
292
|
+
ok: false,
|
|
293
|
+
error: 'Editor context is updating.',
|
|
294
|
+
scene: {
|
|
295
|
+
path: context.scene.path,
|
|
296
|
+
revision,
|
|
297
|
+
},
|
|
298
|
+
}, 409);
|
|
299
|
+
}
|
|
300
|
+
return jsonResponse(context);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function fetchRequest(request: Request) {
|
|
304
|
+
const pathname = new URL(request.url).pathname;
|
|
305
|
+
if (pathname !== '/api/editor-session/health' && pathname !== '/api/editor-context') {
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
if (!authorizationMatches(request, options.token)) {
|
|
309
|
+
return jsonResponse({ ok: false, error: 'Editor session token is invalid.' }, 401);
|
|
310
|
+
}
|
|
311
|
+
if (pathname === '/api/editor-session/health') {
|
|
312
|
+
return jsonResponse({
|
|
313
|
+
protocolVersion: editorSessionProtocolVersion,
|
|
314
|
+
projectRoot,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
return contextResponse();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function notifyProjectFileChanged(changedPath: string) {
|
|
321
|
+
activeBrowser?.send(JSON.stringify({ type: 'projectFileChanged', path: changedPath }));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
open,
|
|
326
|
+
close,
|
|
327
|
+
message,
|
|
328
|
+
fetch: fetchRequest,
|
|
329
|
+
notifyProjectFileChanged,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function defaultEditorSessionsRoot() {
|
|
334
|
+
return path.join(os.tmpdir(), 'pixifact', 'editor-sessions');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function editorSessionDescriptorPath(projectRoot: string, sessionsRoot = defaultEditorSessionsRoot()) {
|
|
338
|
+
const canonicalRoot = fs.realpathSync(projectRoot);
|
|
339
|
+
const projectHash = createHash('sha256').update(canonicalRoot).digest('hex');
|
|
340
|
+
return path.join(sessionsRoot, `${projectHash}.json`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function validateDescriptor(value: unknown): EditorSessionDescriptor {
|
|
344
|
+
if (
|
|
345
|
+
!isRecord(value)
|
|
346
|
+
|| typeof value.protocolVersion !== 'number'
|
|
347
|
+
|| typeof value.projectRoot !== 'string'
|
|
348
|
+
|| typeof value.pid !== 'number'
|
|
349
|
+
|| typeof value.origin !== 'string'
|
|
350
|
+
|| typeof value.token !== 'string'
|
|
351
|
+
) {
|
|
352
|
+
throw new Error('Pixifact Editor session descriptor is invalid.');
|
|
353
|
+
}
|
|
354
|
+
if (value.protocolVersion !== editorSessionProtocolVersion) {
|
|
355
|
+
throw new Error('Editor session protocol version does not match. Restart Pixifact Editor.');
|
|
356
|
+
}
|
|
357
|
+
return value as unknown as EditorSessionDescriptor;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function readEditorSessionDescriptor(projectRoot: string, sessionsRoot = defaultEditorSessionsRoot()) {
|
|
361
|
+
const descriptorPath = editorSessionDescriptorPath(projectRoot, sessionsRoot);
|
|
362
|
+
if (!fs.existsSync(descriptorPath)) {
|
|
363
|
+
return undefined;
|
|
364
|
+
}
|
|
365
|
+
return validateDescriptor(JSON.parse(fs.readFileSync(descriptorPath, 'utf8')));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function writeEditorSessionDescriptor(
|
|
369
|
+
descriptor: EditorSessionDescriptor,
|
|
370
|
+
sessionsRoot = defaultEditorSessionsRoot(),
|
|
371
|
+
) {
|
|
372
|
+
fs.mkdirSync(sessionsRoot, { recursive: true });
|
|
373
|
+
fs.writeFileSync(
|
|
374
|
+
editorSessionDescriptorPath(descriptor.projectRoot, sessionsRoot),
|
|
375
|
+
`${JSON.stringify(descriptor, null, 2)}\n`,
|
|
376
|
+
{ encoding: 'utf8', flag: 'wx', mode: 0o600 },
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function removeEditorSessionDescriptor(
|
|
381
|
+
descriptor: EditorSessionDescriptor,
|
|
382
|
+
sessionsRoot = defaultEditorSessionsRoot(),
|
|
383
|
+
) {
|
|
384
|
+
const descriptorPath = editorSessionDescriptorPath(descriptor.projectRoot, sessionsRoot);
|
|
385
|
+
if (!fs.existsSync(descriptorPath)) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const current = readEditorSessionDescriptor(descriptor.projectRoot, sessionsRoot);
|
|
389
|
+
if (current?.token === descriptor.token) {
|
|
390
|
+
fs.unlinkSync(descriptorPath);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export async function findActiveEditorSession(options: EditorSessionLookupOptions) {
|
|
395
|
+
const projectRoot = fs.realpathSync(options.projectRoot);
|
|
396
|
+
const descriptor = readEditorSessionDescriptor(projectRoot, options.sessionsRoot);
|
|
397
|
+
if (!descriptor) {
|
|
398
|
+
return undefined;
|
|
399
|
+
}
|
|
400
|
+
if (descriptor.projectRoot !== projectRoot) {
|
|
401
|
+
throw new Error('Pixifact Editor session belongs to a different project.');
|
|
402
|
+
}
|
|
403
|
+
const fetcher = options.fetch ?? fetch;
|
|
404
|
+
let response: Response;
|
|
405
|
+
try {
|
|
406
|
+
response = await fetcher(`${descriptor.origin}/api/editor-session/health`, {
|
|
407
|
+
headers: { authorization: `Bearer ${descriptor.token}` },
|
|
408
|
+
});
|
|
409
|
+
} catch {
|
|
410
|
+
return undefined;
|
|
411
|
+
}
|
|
412
|
+
if (!response.ok) {
|
|
413
|
+
return undefined;
|
|
414
|
+
}
|
|
415
|
+
const health = validateDescriptorHealth(await response.json());
|
|
416
|
+
if (health.projectRoot !== projectRoot) {
|
|
417
|
+
throw new Error('Pixifact Editor Host health check returned a different project.');
|
|
418
|
+
}
|
|
419
|
+
return descriptor;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function validateDescriptorHealth(value: unknown) {
|
|
423
|
+
if (!isRecord(value) || typeof value.protocolVersion !== 'number' || typeof value.projectRoot !== 'string') {
|
|
424
|
+
throw new Error('Pixifact Editor Host health response is invalid.');
|
|
425
|
+
}
|
|
426
|
+
if (value.protocolVersion !== editorSessionProtocolVersion) {
|
|
427
|
+
throw new Error('Editor session protocol version does not match. Restart Pixifact Editor.');
|
|
428
|
+
}
|
|
429
|
+
return value as { protocolVersion: number; projectRoot: string };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export async function claimEditorSession(options: ClaimEditorSessionOptions) {
|
|
433
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
434
|
+
const current = readEditorSessionDescriptor(options.projectRoot, options.sessionsRoot);
|
|
435
|
+
const active = await findActiveEditorSession(options);
|
|
436
|
+
if (active) {
|
|
437
|
+
return { descriptor: active, owned: false as const };
|
|
438
|
+
}
|
|
439
|
+
if (current) {
|
|
440
|
+
removeEditorSessionDescriptor(current, options.sessionsRoot);
|
|
441
|
+
}
|
|
442
|
+
try {
|
|
443
|
+
writeEditorSessionDescriptor(options.descriptor, options.sessionsRoot);
|
|
444
|
+
return { descriptor: options.descriptor, owned: true as const };
|
|
445
|
+
} catch (error) {
|
|
446
|
+
if (!isRecord(error) || error.code !== 'EEXIST') {
|
|
447
|
+
throw error;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
throw new Error('Pixifact Editor session could not be registered.');
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export async function queryEditorContext(options: EditorSessionLookupOptions) {
|
|
455
|
+
const projectRoot = fs.realpathSync(options.projectRoot);
|
|
456
|
+
const descriptor = readEditorSessionDescriptor(projectRoot, options.sessionsRoot);
|
|
457
|
+
if (!descriptor) {
|
|
458
|
+
return {
|
|
459
|
+
ok: false,
|
|
460
|
+
error: 'No Pixifact Editor Host is running for this project.',
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
const fetcher = options.fetch ?? fetch;
|
|
464
|
+
try {
|
|
465
|
+
const response = await fetcher(`${descriptor.origin}/api/editor-context`, {
|
|
466
|
+
headers: { authorization: `Bearer ${descriptor.token}` },
|
|
467
|
+
});
|
|
468
|
+
const result = await response.json() as Record<string, unknown>;
|
|
469
|
+
if (response.ok) {
|
|
470
|
+
if (result.protocolVersion !== editorSessionProtocolVersion) {
|
|
471
|
+
return {
|
|
472
|
+
ok: false,
|
|
473
|
+
error: 'Editor session protocol version does not match. Restart Pixifact Editor.',
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
if (result.projectRoot !== projectRoot) {
|
|
477
|
+
return {
|
|
478
|
+
ok: false,
|
|
479
|
+
error: 'Pixifact Editor context belongs to a different project.',
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return result;
|
|
484
|
+
} catch {
|
|
485
|
+
return {
|
|
486
|
+
ok: false,
|
|
487
|
+
error: 'Pixifact Editor Host is not reachable for this project.',
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
}
|