phibelle-kit 1.0.28 → 1.0.30

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.
@@ -2,9 +2,11 @@ import chalk from "chalk";
2
2
  import * as path from "path";
3
3
  import * as fs from "fs";
4
4
  import { WebSocketServer } from "ws";
5
- import { unpackageScene, parseSceneJson } from "../lib/scene/scene-packaging.js";
5
+ import { unpackageScene, parseSceneJson, packageSceneFromFilesystem } from "../lib/scene/scene-packaging.js";
6
6
  import { createScriptWatcher } from "../lib/scene/script-watcher.js";
7
7
  import { toErrorMessage } from "../lib/utils.js";
8
+ import readline from "readline";
9
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
8
10
  const WS_PORT = 31113;
9
11
  const SCENE_SYNC_TYPE = "phibelle-kit-scene-sync";
10
12
  const SCENE_FILE_NAME = "scene.phibelle";
@@ -22,7 +24,7 @@ export async function watchSceneCommand() {
22
24
  console.log();
23
25
  console.log(chalk.gray(" Press Ctrl+C to stop"));
24
26
  console.log();
25
- let latestSceneJson = "";
27
+ let latestSceneJson = readLocalSceneJson(sceneDir);
26
28
  let scriptWatcher = null;
27
29
  let currentClient = null;
28
30
  function startWatcher() {
@@ -36,6 +38,9 @@ export async function watchSceneCommand() {
36
38
  const wss = new WebSocketServer({ port: WS_PORT });
37
39
  wss.on("connection", (ws) => {
38
40
  currentClient = ws;
41
+ console.log(chalk.green(" ✓ Connected to web app"));
42
+ let initialSyncResolved = false;
43
+ let initialSyncPending = false;
39
44
  ws.on("message", async (data) => {
40
45
  const raw = Buffer.isBuffer(data) ? data.toString("utf8") : String(data);
41
46
  if (!raw || raw.length === 0)
@@ -49,6 +54,47 @@ export async function watchSceneCommand() {
49
54
  catch {
50
55
  return;
51
56
  }
57
+ if (!initialSyncResolved) {
58
+ if (initialSyncPending)
59
+ return;
60
+ initialSyncPending = true;
61
+ try {
62
+ latestSceneJson = await packageAndPersistLocalScene(sceneDir, latestSceneJson);
63
+ }
64
+ catch (error) {
65
+ initialSyncPending = false;
66
+ console.log(chalk.red(" ✖ Failed to package local scene: " + toErrorMessage(error)));
67
+ return;
68
+ }
69
+ if (incoming.sceneData === latestSceneJson) {
70
+ initialSyncResolved = true;
71
+ initialSyncPending = false;
72
+ startWatcher();
73
+ return;
74
+ }
75
+ const choice = await promptForInitialSyncChoice().catch((error) => {
76
+ console.log(chalk.red(" ✖ " + toErrorMessage(error)));
77
+ ws.close();
78
+ return null;
79
+ });
80
+ if (!choice || currentClient !== ws)
81
+ return;
82
+ if (choice === "push-local") {
83
+ initialSyncResolved = true;
84
+ initialSyncPending = false;
85
+ if (ws.readyState === ws.OPEN) {
86
+ ws.send(latestSceneJson);
87
+ startWatcher();
88
+ console.log(chalk.green(" ✓ Pushed local scene to app"));
89
+ }
90
+ return;
91
+ }
92
+ initialSyncResolved = true;
93
+ initialSyncPending = false;
94
+ console.log(chalk.cyan(" Accepting scene from web app..."));
95
+ }
96
+ if (currentClient !== ws)
97
+ return;
52
98
  latestSceneJson = incoming.sceneData;
53
99
  const scenePath = path.join(sceneDir, SCENE_FILE_NAME);
54
100
  fs.writeFileSync(scenePath, incoming.sceneData, "utf8");
@@ -73,6 +119,7 @@ export async function watchSceneCommand() {
73
119
  scriptWatcher?.close();
74
120
  scriptWatcher = null;
75
121
  wss.close();
122
+ rl.close();
76
123
  console.log(chalk.gray(" Goodbye"));
77
124
  console.log();
78
125
  process.exit(0);
@@ -106,6 +153,39 @@ function printEditorLink(sceneId, lastPrintedLink) {
106
153
  console.log(chalk.cyan(` Editor: ${editorUrl}`));
107
154
  return editorUrl;
108
155
  }
156
+ function readLocalSceneJson(sceneDir) {
157
+ const scenePath = path.join(sceneDir, SCENE_FILE_NAME);
158
+ if (!fs.existsSync(scenePath)) {
159
+ throw new Error(`Missing ${SCENE_FILE_NAME} in ${sceneDir}. Run "phibelle-kit clone" first.`);
160
+ }
161
+ const sceneJson = fs.readFileSync(scenePath, "utf8");
162
+ parseSceneJson(sceneJson);
163
+ return sceneJson;
164
+ }
165
+ async function packageAndPersistLocalScene(sceneDir, sceneJson) {
166
+ const packagedSceneJson = await packageSceneFromFilesystem(sceneJson, sceneDir);
167
+ parseSceneJson(packagedSceneJson);
168
+ fs.writeFileSync(path.join(sceneDir, SCENE_FILE_NAME), packagedSceneJson, "utf8");
169
+ return packagedSceneJson;
170
+ }
171
+ async function promptForInitialSyncChoice() {
172
+ console.log(chalk.yellow(" Choose initial sync direction:"));
173
+ console.log(chalk.cyan(" [p] Push local scene to the web app"));
174
+ console.log(chalk.cyan(" [a] Accept the current web app scene"));
175
+ while (true) {
176
+ const answer = (await askQuestion(" Initial sync [p/a]: ")).trim().toLowerCase();
177
+ if (answer === "p" || answer === "push" || answer === "local")
178
+ return "push-local";
179
+ if (answer === "a" || answer === "accept" || answer === "web" || answer === "app")
180
+ return "accept-web";
181
+ console.log(chalk.yellow(" Please enter 'p' to push local or 'a' to accept the web app scene."));
182
+ }
183
+ }
184
+ function askQuestion(prompt) {
185
+ return new Promise((resolve) => {
186
+ rl.question(prompt, resolve);
187
+ });
188
+ }
109
189
  function getRequiredManifestSceneId(sceneDir) {
110
190
  const manifestPath = path.join(sceneDir, MANIFEST_FILE_NAME);
111
191
  if (!fs.existsSync(manifestPath)) {
@@ -18,3 +18,4 @@ export declare function unpackageScene(sceneJson: string, sceneDir: string, scen
18
18
  * Used by script watcher to produce scene snapshot to send to FE.
19
19
  */
20
20
  export declare function patchSceneFromFile(sceneJson: string, sceneDir: string, relativePath: string, eventType: "change" | "add"): Promise<string>;
21
+ export declare function packageSceneFromFilesystem(sceneJson: string, sceneDir: string): Promise<string>;
@@ -133,6 +133,8 @@ export async function patchSceneFromFile(sceneJson, sceneDir, relativePath, even
133
133
  const fileContent = await fs.readFile(filePath, "utf8");
134
134
  if (!parsed.sceneScriptStore)
135
135
  parsed.sceneScriptStore = { sceneScript: "", sceneProperties: [], lastModifiedSceneScript: 0 };
136
+ if (parsed.sceneScriptStore.sceneScript === fileContent)
137
+ return sceneJson;
136
138
  parsed.sceneScriptStore.sceneScript = fileContent;
137
139
  parsed.sceneScriptStore.lastModifiedSceneScript = Date.now();
138
140
  return JSON.stringify(parsed);
@@ -142,7 +144,11 @@ export async function patchSceneFromFile(sceneJson, sceneDir, relativePath, even
142
144
  const sceneProperties = JSON.parse(fileContent);
143
145
  if (!parsed.sceneScriptStore)
144
146
  parsed.sceneScriptStore = { sceneScript: "", sceneProperties: [], lastModifiedSceneScript: 0 };
145
- parsed.sceneScriptStore.sceneProperties = Array.isArray(sceneProperties) ? sceneProperties : [];
147
+ const nextSceneProperties = Array.isArray(sceneProperties) ? sceneProperties : [];
148
+ if (JSON.stringify(parsed.sceneScriptStore.sceneProperties ?? []) === JSON.stringify(nextSceneProperties)) {
149
+ return sceneJson;
150
+ }
151
+ parsed.sceneScriptStore.sceneProperties = nextSceneProperties;
146
152
  return JSON.stringify(parsed);
147
153
  }
148
154
  const scriptMatch = relativePath.match(new RegExp(`^([^/]+)\\/${ENTITY_SCRIPT_FILE.replace(".", "\\.")}$`));
@@ -155,6 +161,8 @@ export async function patchSceneFromFile(sceneJson, sceneDir, relativePath, even
155
161
  const entity = entities[existingId];
156
162
  if (!entity)
157
163
  return sceneJson;
164
+ if (entity.script === fileContent)
165
+ return sceneJson;
158
166
  entity.script = fileContent;
159
167
  entity.scriptVersion = (entity.scriptVersion ?? 0) + 1;
160
168
  return JSON.stringify(parsed);
@@ -175,7 +183,10 @@ export async function patchSceneFromFile(sceneJson, sceneDir, relativePath, even
175
183
  const entity = entities[entityId];
176
184
  if (!entity)
177
185
  return sceneJson;
178
- entity.properties = Array.isArray(properties) ? properties : [];
186
+ const nextProperties = Array.isArray(properties) ? properties : [];
187
+ if (JSON.stringify(entity.properties ?? []) === JSON.stringify(nextProperties))
188
+ return sceneJson;
189
+ entity.properties = nextProperties;
179
190
  return JSON.stringify(parsed);
180
191
  }
181
192
  const transformsMatch = relativePath.match(new RegExp(`^([^/]+)\\/${ENTITY_TRANSFORMS_FILE.replace(".", "\\.")}$`));
@@ -191,16 +202,63 @@ export async function patchSceneFromFile(sceneJson, sceneDir, relativePath, even
191
202
  return sceneJson;
192
203
  if (!entity.transform)
193
204
  entity.transform = { position: DEFAULT_POSITION, rotation: DEFAULT_ROTATION, scale: DEFAULT_SCALE };
194
- if (data.position !== undefined)
195
- entity.transform.position = JSON.stringify(data.position);
196
- if (data.rotation !== undefined)
197
- entity.transform.rotation = JSON.stringify(data.rotation);
198
- if (data.scale !== undefined)
199
- entity.transform.scale = JSON.stringify(data.scale);
205
+ const nextPosition = data.position !== undefined ? JSON.stringify(data.position) : entity.transform.position;
206
+ const nextRotation = data.rotation !== undefined ? JSON.stringify(data.rotation) : entity.transform.rotation;
207
+ const nextScale = data.scale !== undefined ? JSON.stringify(data.scale) : entity.transform.scale;
208
+ if (entity.transform.position === nextPosition &&
209
+ entity.transform.rotation === nextRotation &&
210
+ entity.transform.scale === nextScale) {
211
+ return sceneJson;
212
+ }
213
+ entity.transform.position = nextPosition;
214
+ entity.transform.rotation = nextRotation;
215
+ entity.transform.scale = nextScale;
200
216
  return JSON.stringify(parsed);
201
217
  }
202
218
  return sceneJson;
203
219
  }
220
+ export async function packageSceneFromFilesystem(sceneJson, sceneDir) {
221
+ const manifestRaw = await fs.readFile(path.join(sceneDir, "manifest.json"), "utf8");
222
+ const manifest = JSON.parse(manifestRaw);
223
+ const appDir = getSceneAppDir(sceneDir);
224
+ let packagedSceneJson = sceneJson;
225
+ packagedSceneJson = await applyIfExists(packagedSceneJson, sceneDir, manifest.sceneScript ?? SCENE_SCRIPT_FILE);
226
+ packagedSceneJson = await applyIfExists(packagedSceneJson, sceneDir, manifest.sceneProperties ?? SCENE_PROPERTIES_FILE);
227
+ for (const folderName of Object.values(manifest.entities)) {
228
+ packagedSceneJson = await applyIfExists(packagedSceneJson, sceneDir, path.join(folderName, ENTITY_SCRIPT_FILE).replace(/\\/g, "/"));
229
+ packagedSceneJson = await applyIfExists(packagedSceneJson, sceneDir, path.join(folderName, ENTITY_PROPERTIES_FILE).replace(/\\/g, "/"));
230
+ packagedSceneJson = await applyIfExists(packagedSceneJson, sceneDir, path.join(folderName, ENTITY_TRANSFORMS_FILE).replace(/\\/g, "/"));
231
+ }
232
+ let appEntries = [];
233
+ try {
234
+ appEntries = await fs.readdir(appDir, { withFileTypes: true });
235
+ }
236
+ catch {
237
+ return packagedSceneJson;
238
+ }
239
+ for (const entry of appEntries) {
240
+ if (!entry.isDirectory())
241
+ continue;
242
+ if (Object.values(manifest.entities).includes(entry.name))
243
+ continue;
244
+ const relativeScriptPath = path.join(entry.name, ENTITY_SCRIPT_FILE).replace(/\\/g, "/");
245
+ packagedSceneJson = await applyIfExists(packagedSceneJson, sceneDir, relativeScriptPath, "add");
246
+ packagedSceneJson = await applyIfExists(packagedSceneJson, sceneDir, path.join(entry.name, ENTITY_PROPERTIES_FILE).replace(/\\/g, "/"));
247
+ packagedSceneJson = await applyIfExists(packagedSceneJson, sceneDir, path.join(entry.name, ENTITY_TRANSFORMS_FILE).replace(/\\/g, "/"));
248
+ }
249
+ return packagedSceneJson;
250
+ }
251
+ async function applyIfExists(sceneJson, sceneDir, relativePath, eventType = "change") {
252
+ const appDir = getSceneAppDir(sceneDir);
253
+ const filePath = path.join(appDir, relativePath);
254
+ try {
255
+ await fs.access(filePath);
256
+ }
257
+ catch {
258
+ return sceneJson;
259
+ }
260
+ return patchSceneFromFile(sceneJson, sceneDir, relativePath, eventType);
261
+ }
204
262
  async function addNewEntityFromFolder(parsed, manifest, sceneDir, folderName, appDir) {
205
263
  if (Object.values(manifest.entities).includes(folderName))
206
264
  return JSON.stringify(parsed);
@@ -1 +1 @@
1
- export declare const TSCONFIG_FILE = "{\n \"compilerOptions\": {\n \"jsx\": \"react-jsx\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"bundler\",\n \"esModuleInterop\": true,\n \"allowUmdGlobalAccess\": true,\n \"baseUrl\": \".\",\n \"skipLibCheck\": true,\n \"lib\": [\"ES2015\", \"DOM\"]\n },\n \"include\": [\n \"app/**/*.tsx\",\n \"global.d.ts\"\n ]\n}";
1
+ export declare const TSCONFIG_FILE = "{\n \"compilerOptions\": {\n \"jsx\": \"react-jsx\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"bundler\",\n \"esModuleInterop\": true,\n \"allowUmdGlobalAccess\": true,\n \"baseUrl\": \".\",\n \"skipLibCheck\": true,\n \"lib\": [\"ES2017\", \"DOM\"]\n },\n \"include\": [\n \"app/**/*.tsx\",\n \"global.d.ts\"\n ]\n}";
@@ -7,7 +7,7 @@ export const TSCONFIG_FILE = `{
7
7
  "allowUmdGlobalAccess": true,
8
8
  "baseUrl": ".",
9
9
  "skipLibCheck": true,
10
- "lib": ["ES2015", "DOM"]
10
+ "lib": ["ES2017", "DOM"]
11
11
  },
12
12
  "include": [
13
13
  "app/**/*.tsx",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phibelle-kit",
3
- "version": "1.0.28",
3
+ "version": "1.0.30",
4
4
  "description": "CLI tool for interacting with the Phibelle engine",
5
5
  "type": "module",
6
6
  "bin": {