phibelle-kit 1.0.22 → 1.0.24

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.
Files changed (36) hide show
  1. package/dist/commands/clone-scene.d.ts +1 -5
  2. package/dist/commands/clone-scene.js +124 -31
  3. package/dist/commands/init-scene.d.ts +1 -0
  4. package/dist/commands/init-scene.js +16 -0
  5. package/dist/commands/watch-scene.d.ts +2 -1
  6. package/dist/commands/watch-scene.js +116 -83
  7. package/dist/constants.d.ts +2 -0
  8. package/dist/constants.js +2 -0
  9. package/dist/index.js +14 -18
  10. package/dist/lib/api/index.d.ts +1 -2
  11. package/dist/lib/api/index.js +1 -2
  12. package/dist/lib/api/public-env.d.ts +0 -1
  13. package/dist/lib/api/public-env.js +0 -1
  14. package/dist/lib/scene/file-system.d.ts +2 -1
  15. package/dist/lib/scene/file-system.js +5 -4
  16. package/dist/lib/scene/first-run-setup.d.ts +5 -1
  17. package/dist/lib/scene/first-run-setup.js +10 -4
  18. package/dist/lib/scene/index.d.ts +1 -2
  19. package/dist/lib/scene/index.js +1 -2
  20. package/dist/lib/scene/scene-packaging.d.ts +9 -15
  21. package/dist/lib/scene/scene-packaging.js +124 -265
  22. package/dist/lib/scene/scene-state.d.ts +42 -0
  23. package/dist/lib/scene/scene-state.js +125 -0
  24. package/dist/lib/scene/script-watcher.d.ts +1 -1
  25. package/dist/lib/scene/script-watcher.js +13 -94
  26. package/dist/lib/ws/index.d.ts +1 -0
  27. package/dist/lib/ws/index.js +1 -0
  28. package/dist/lib/ws/ws-server.d.ts +15 -0
  29. package/dist/lib/ws/ws-server.js +56 -0
  30. package/dist/scene-setup/agents-md.d.ts +1 -1
  31. package/dist/scene-setup/agents-md.js +35 -21
  32. package/dist/scene-setup/npm-package.d.ts +1 -1
  33. package/dist/scene-setup/npm-package.js +2 -2
  34. package/dist/scene-setup/setup.d.ts +1 -1
  35. package/dist/scene-setup/setup.js +6 -6
  36. package/package.json +3 -4
@@ -1,5 +1 @@
1
- /**
2
- * Clone: one-time fetch of scene data. Creates scene dir, writes scene.json,
3
- * sets up package.json/global.d.ts, unpacks scripts. No watcher, no opening folder.
4
- */
5
- export declare function cloneSceneCommand(sceneId: string): Promise<void>;
1
+ export declare function cloneSceneCommand(): Promise<void>;
@@ -1,37 +1,130 @@
1
1
  import chalk from "chalk";
2
- import * as path from "path";
3
- import { queryOnce, BASE_URL } from "../lib/api/index.js";
4
- import { api } from "../convex/_generated/api.js";
5
- import { sceneNameToFolder, setupSceneFiles, unpackageWithLogging } from "../lib/scene/index.js";
6
- /**
7
- * Clone: one-time fetch of scene data. Creates scene dir, writes scene.json,
8
- * sets up package.json/global.d.ts, unpacks scripts. No watcher, no opening folder.
9
- */
10
- export async function cloneSceneCommand(sceneId) {
2
+ import * as path from "node:path";
3
+ import * as fs from "node:fs/promises";
4
+ import { WebSocketServer } from "ws";
5
+ import { parseSceneJson, unpackageScene } from "../lib/scene/scene-packaging.js";
6
+ import { setupSceneDirectory } from "../scene-setup/setup.js";
7
+ const WS_PORT = 31113;
8
+ const CLONE_REQUEST_TYPE = "phibelle-kit-clone-request";
9
+ const CLONE_RESPONSE_TYPE = "phibelle-kit-clone-response";
10
+ const CLONE_TIMEOUT_MS = 30_000;
11
+ export async function cloneSceneCommand() {
12
+ const parentDir = process.cwd();
11
13
  console.log();
12
- console.log(chalk.bold.cyan(" 📥 Cloning scene..."));
13
- console.log(chalk.gray(` Scene ID: ${sceneId}`));
14
+ console.log(chalk.bold.cyan(" Cloning scene from editor..."));
15
+ console.log(chalk.gray(` Target parent dir: ${parentDir}`));
16
+ console.log(chalk.yellow(" Keep the editor open while cloning and make sure the WebSocket is enabled."));
14
17
  console.log();
15
- const scene = await queryOnce(api.scenes.getById, {
16
- id: sceneId,
18
+ const payload = await requestClonePayload();
19
+ validateClonePayload(payload);
20
+ const safeDirName = sanitizeDirectoryName(payload.sceneName);
21
+ const sceneDir = path.join(parentDir, safeDirName);
22
+ await ensureDirectoryDoesNotExist(sceneDir);
23
+ await fs.mkdir(sceneDir, { recursive: false });
24
+ setupSceneDirectory(sceneDir);
25
+ await fs.writeFile(path.join(sceneDir, "scene.phibelle"), payload.sceneData, "utf8");
26
+ await unpackageScene(payload.sceneData, sceneDir, payload.sceneId);
27
+ console.log(chalk.green(" ✓ Scene cloned successfully"));
28
+ console.log(chalk.gray(` Directory: ${sceneDir}`));
29
+ console.log(chalk.yellow(" Next steps:"));
30
+ console.log(chalk.cyan(` 1. cd "${safeDirName}"`));
31
+ console.log(chalk.cyan(` 2. npm install`));
32
+ console.log(chalk.cyan(` 3. npm run watch`));
33
+ console.log();
34
+ process.exit(0);
35
+ }
36
+ function requestClonePayload() {
37
+ return new Promise((resolve, reject) => {
38
+ let settled = false;
39
+ const wss = new WebSocketServer({ port: WS_PORT });
40
+ const closeServer = () => {
41
+ try {
42
+ wss.close();
43
+ }
44
+ catch {
45
+ // no-op
46
+ }
47
+ };
48
+ const timeout = setTimeout(() => {
49
+ if (settled)
50
+ return;
51
+ settled = true;
52
+ closeServer();
53
+ reject(new Error("Timed out waiting for scene payload from editor. Make sure the editor is open on the scene you want to clone."));
54
+ }, CLONE_TIMEOUT_MS);
55
+ wss.on("connection", (ws) => {
56
+ ws.send(JSON.stringify({ type: CLONE_REQUEST_TYPE }));
57
+ ws.on("message", (data) => {
58
+ if (settled)
59
+ return;
60
+ const raw = Buffer.isBuffer(data) ? data.toString("utf8") : String(data);
61
+ let parsed;
62
+ try {
63
+ parsed = JSON.parse(raw);
64
+ }
65
+ catch {
66
+ return;
67
+ }
68
+ if (!isCloneResponse(parsed))
69
+ return;
70
+ settled = true;
71
+ clearTimeout(timeout);
72
+ closeServer();
73
+ resolve(parsed);
74
+ });
75
+ ws.on("error", (err) => {
76
+ if (settled)
77
+ return;
78
+ settled = true;
79
+ clearTimeout(timeout);
80
+ closeServer();
81
+ reject(err);
82
+ });
83
+ });
84
+ wss.on("error", (err) => {
85
+ if (settled)
86
+ return;
87
+ settled = true;
88
+ clearTimeout(timeout);
89
+ closeServer();
90
+ reject(err);
91
+ });
17
92
  });
18
- if (scene === null) {
19
- console.log(chalk.red(" ✖ Scene not found"));
20
- console.log();
21
- process.exit(1);
93
+ }
94
+ function isCloneResponse(value) {
95
+ if (typeof value !== "object" || value === null)
96
+ return false;
97
+ const obj = value;
98
+ return (obj.type === CLONE_RESPONSE_TYPE &&
99
+ typeof obj.sceneName === "string" &&
100
+ typeof obj.sceneId === "string" &&
101
+ typeof obj.sceneData === "string");
102
+ }
103
+ function validateClonePayload(payload) {
104
+ if (!payload.sceneName.trim()) {
105
+ throw new Error("Clone payload is missing a valid scene name.");
22
106
  }
23
- const sceneFolderName = sceneNameToFolder(scene.name);
24
- const sceneDir = path.join(process.cwd(), sceneFolderName);
25
- setupSceneFiles(scene, sceneDir, true);
26
- await unpackageWithLogging(scene._id, sceneDir, "Edit the scene in the app once to create a scene file, then run clone again.");
27
- console.log(chalk.green(" ✓ Cloned scene"));
28
- console.log(chalk.white(` Name: ${scene.name}`));
29
- console.log(chalk.white(` Path: ${sceneDir}`));
30
- console.log(chalk.white(" Scene link: ") + chalk.cyan(`${BASE_URL}/editor?sceneId=${scene._id}`));
31
- console.log();
32
- console.log(chalk.gray(" Next steps:"));
33
- console.log(chalk.gray(" 1. ") + chalk.blueBright("cd ") + chalk.cyan(sceneFolderName) + chalk.gray(" to open the scene directory."));
34
- console.log(chalk.gray(" 2. ") + chalk.blueBright("npm install") + chalk.gray(" to install the dependencies."));
35
- console.log(chalk.gray(" 3. ") + chalk.blueBright("npm run watch ") + chalk.gray(" to watch for changes and push edits."));
36
- console.log();
107
+ if (!payload.sceneId.trim()) {
108
+ throw new Error("Clone payload is missing a valid scene id.");
109
+ }
110
+ parseSceneJson(payload.sceneData);
111
+ }
112
+ function sanitizeDirectoryName(sceneName) {
113
+ const sanitized = sceneName
114
+ .trim()
115
+ .replace(/[<>:"/\\|?*\x00-\x1F]/g, "-")
116
+ .replace(/\s+/g, " ")
117
+ .replace(/\.+$/g, "");
118
+ if (!sanitized)
119
+ return "scene-clone";
120
+ return sanitized;
121
+ }
122
+ async function ensureDirectoryDoesNotExist(sceneDir) {
123
+ try {
124
+ await fs.access(sceneDir);
125
+ }
126
+ catch {
127
+ return;
128
+ }
129
+ throw new Error(`Target folder already exists: ${sceneDir}. Rename the scene or clone into another directory.`);
37
130
  }
@@ -0,0 +1 @@
1
+ export declare function initSceneCommand(): void;
@@ -0,0 +1,16 @@
1
+ import chalk from "chalk";
2
+ import { setupSceneDirectory, getInstallHint } from "../scene-setup/setup.js";
3
+ export function initSceneCommand() {
4
+ const sceneDir = process.cwd();
5
+ console.log();
6
+ console.log(chalk.bold.cyan(" Initializing scene..."));
7
+ console.log(chalk.gray(` Dir: ${sceneDir}`));
8
+ console.log();
9
+ const { shouldPromptInstall } = setupSceneDirectory(sceneDir);
10
+ console.log(chalk.green(" ✓ Created package.json, tsconfig.json, global.d.ts, AGENTS.md"));
11
+ if (shouldPromptInstall) {
12
+ console.log(chalk.yellow(" Install dependencies: ") + chalk.cyan(getInstallHint(sceneDir)));
13
+ }
14
+ console.log(chalk.gray(" Next: open a scene in the app, then run ") + chalk.cyan("pnpm run watch") + chalk.gray(" to sync."));
15
+ console.log();
16
+ }
@@ -1 +1,2 @@
1
- export declare function watchSceneCommand(sceneId: string): Promise<void>;
1
+ export declare const BASE_URL: string;
2
+ export declare function watchSceneCommand(): Promise<void>;
@@ -1,99 +1,132 @@
1
1
  import chalk from "chalk";
2
2
  import * as path from "path";
3
3
  import * as fs from "fs";
4
- import { subscribeToQuery, BASE_URL } from "../lib/api/index.js";
5
- import { api } from "../convex/_generated/api.js";
6
- import { getSessionId } from "../lib/auth/index.js";
7
- import { findSceneDirBySceneId, sceneNameToFolder, unpackageScene, createScriptWatcher, runFirstUpdateSetup, } from "../lib/scene/index.js";
4
+ import { WebSocketServer } from "ws";
5
+ import { unpackageScene, parseSceneJson } from "../lib/scene/scene-packaging.js";
6
+ import { createScriptWatcher } from "../lib/scene/script-watcher.js";
8
7
  import { toErrorMessage } from "../lib/utils.js";
9
- function createWatchCallbacks() {
10
- return {
11
- onPushed: (file) => console.log(chalk.green(" ✓ Pushed " + file + " to Convex")),
12
- onNewEntity: (engineId, fileName) => console.log(chalk.green(" ✓ Created new entity " + engineId + (fileName ? " (" + fileName + ")" : ""))),
13
- onError: (file, err) => console.log(chalk.red(" ✖ Failed to push " + file + ": " + err.message)),
14
- };
15
- }
16
- function startScriptWatcher(scene, sceneDir) {
17
- const c = createWatchCallbacks();
18
- return createScriptWatcher(scene._id, sceneDir, c.onPushed, c.onNewEntity, c.onError);
19
- }
20
- export async function watchSceneCommand(sceneId) {
8
+ const WS_PORT = 31113;
9
+ const SCENE_SYNC_TYPE = "phibelle-kit-scene-sync";
10
+ const SCENE_FILE_NAME = "scene.phibelle";
11
+ const MANIFEST_FILE_NAME = "manifest.json";
12
+ export const BASE_URL = process.env.NODE_ENV === "development" ? "http://localhost:3131" : "https://phibelle.studio";
13
+ export async function watchSceneCommand() {
14
+ const sceneDir = process.cwd();
15
+ const manifestSceneId = getRequiredManifestSceneId(sceneDir);
16
+ let lastPrintedEditorLink = null;
17
+ console.log();
18
+ console.log(chalk.bold.cyan(" Watching for scene sync (WebSocket)"));
19
+ console.log(chalk.gray(` Dir: ${sceneDir}`));
20
+ lastPrintedEditorLink = printEditorLink(manifestSceneId, lastPrintedEditorLink);
21
+ console.log(chalk.yellow(" Waiting for connection from local development server..."));
21
22
  console.log();
22
- console.log(chalk.bold.cyan(" 👀 Watching scene updates..."));
23
- console.log(chalk.gray(` Scene ID: ${sceneId}`));
24
- console.log(chalk.white(" Scene link: ") + chalk.cyan(`${BASE_URL}/editor?sceneId=${sceneId}`));
25
- let previousUpdatedAt = null;
26
- let isFirstUpdate = true;
23
+ console.log(chalk.gray(" Press Ctrl+C to stop"));
24
+ console.log();
25
+ let latestSceneJson = "";
27
26
  let scriptWatcher = null;
28
- try {
29
- const unsubscribe = subscribeToQuery(api.scenes.getById, { id: sceneId }, async (scene) => {
30
- if (scene === null) {
31
- if (isFirstUpdate) {
32
- console.log(chalk.red(" ✖ Scene not found"));
33
- console.log();
34
- process.exit(1);
35
- }
27
+ let currentClient = null;
28
+ function startWatcher() {
29
+ scriptWatcher?.close();
30
+ scriptWatcher = createScriptWatcher(sceneDir, () => latestSceneJson, (json) => {
31
+ latestSceneJson = json;
32
+ if (currentClient && currentClient.readyState === 1)
33
+ currentClient.send(json);
34
+ }, (file, err) => console.log(chalk.red(" ✖ " + file + ": " + err.message)));
35
+ }
36
+ const wss = new WebSocketServer({ port: WS_PORT });
37
+ wss.on("connection", (ws) => {
38
+ currentClient = ws;
39
+ ws.on("message", async (data) => {
40
+ const raw = Buffer.isBuffer(data) ? data.toString("utf8") : String(data);
41
+ if (!raw || raw.length === 0)
42
+ return;
43
+ const incoming = parseIncomingSceneMessage(raw);
44
+ if (!incoming)
45
+ return;
46
+ try {
47
+ parseSceneJson(incoming.sceneData);
48
+ }
49
+ catch {
36
50
  return;
37
51
  }
38
- const currentUpdatedAt = scene.updatedAt;
39
- const cwd = process.cwd();
40
- const sceneDir = findSceneDirBySceneId(cwd, sceneId) ?? path.join(cwd, sceneNameToFolder(scene.name));
41
- const manifestPath = path.join(sceneDir, "manifest.json");
42
- const alreadyCloned = fs.existsSync(manifestPath);
43
- if (isFirstUpdate) {
44
- if (alreadyCloned) {
45
- fs.writeFileSync(path.join(sceneDir, "scene.json"), JSON.stringify(scene));
46
- scriptWatcher = startScriptWatcher(scene, sceneDir);
47
- console.log(chalk.green(" ✓ Watching scene (previously cloned)"));
48
- console.log(chalk.white(` Name: ${scene.name}`));
49
- console.log(chalk.white(` Dir: ${sceneDir}`));
50
- console.log(chalk.gray(" Press Ctrl+C to stop"));
51
- console.log();
52
- }
53
- else {
54
- scriptWatcher = await runFirstUpdateSetup(scene, sceneDir, createWatchCallbacks());
55
- }
56
- previousUpdatedAt = currentUpdatedAt;
57
- isFirstUpdate = false;
52
+ latestSceneJson = incoming.sceneData;
53
+ const scenePath = path.join(sceneDir, SCENE_FILE_NAME);
54
+ fs.writeFileSync(scenePath, incoming.sceneData, "utf8");
55
+ try {
56
+ await unpackageScene(incoming.sceneData, sceneDir, incoming.sceneId);
57
+ console.log(chalk.green(" ✓ Synced scene from app"));
58
+ const updatedManifestSceneId = getRequiredManifestSceneId(sceneDir);
59
+ lastPrintedEditorLink = printEditorLink(updatedManifestSceneId, lastPrintedEditorLink);
58
60
  }
59
- else if (previousUpdatedAt !== null && currentUpdatedAt !== previousUpdatedAt) {
60
- fs.writeFileSync(path.join(sceneDir, "scene.json"), JSON.stringify(scene));
61
- console.log(chalk.green(" ✓ Scene updated"));
62
- console.log(chalk.white(` Timestamp: ${new Date(currentUpdatedAt).toLocaleString()}`));
63
- const ourSessionId = getSessionId();
64
- const weSaved = scene.lastModifiedSessionId != null && scene.lastModifiedSessionId === ourSessionId;
65
- if (!weSaved) {
66
- scriptWatcher?.close();
67
- scriptWatcher = null;
68
- try {
69
- await unpackageScene(scene._id, sceneDir);
70
- console.log(chalk.green(" ✓ Re-unpackaged scene (scene-script.tsx, scene-properties.json, entity folders with script.tsx, properties.json, transforms.json, manifest.json)"));
71
- scriptWatcher = startScriptWatcher(scene, sceneDir);
72
- }
73
- catch (e) {
74
- console.log(chalk.yellow(" ⚠ Could not re-unpackage scene scripts: " + toErrorMessage(e)));
75
- }
76
- }
77
- console.log();
78
- previousUpdatedAt = currentUpdatedAt;
61
+ catch (e) {
62
+ console.log(chalk.yellow(" ⚠ Unpackage: " + toErrorMessage(e)));
79
63
  }
64
+ startWatcher();
80
65
  });
81
- process.on("SIGINT", () => {
82
- console.log();
83
- console.log(chalk.gray(" Unsubscribing..."));
84
- scriptWatcher?.close();
85
- scriptWatcher = null;
86
- unsubscribe();
87
- console.log(chalk.gray(" Goodbye! 👋"));
88
- console.log();
89
- process.exit(0);
66
+ ws.on("close", () => {
67
+ if (currentClient === ws)
68
+ currentClient = null;
90
69
  });
91
- await new Promise(() => { });
92
- }
93
- catch (error) {
70
+ });
71
+ process.on("SIGINT", () => {
94
72
  console.log();
95
- console.log(chalk.red(` ✖ Error: ${toErrorMessage(error)}`));
73
+ scriptWatcher?.close();
74
+ scriptWatcher = null;
75
+ wss.close();
76
+ console.log(chalk.gray(" Goodbye"));
96
77
  console.log();
97
- throw error;
78
+ process.exit(0);
79
+ });
80
+ await new Promise(() => { });
81
+ }
82
+ function parseIncomingSceneMessage(raw) {
83
+ try {
84
+ const parsed = JSON.parse(raw);
85
+ if (isSceneSyncMessage(parsed)) {
86
+ return { sceneData: parsed.sceneData, sceneId: parsed.sceneId?.trim() || undefined };
87
+ }
88
+ }
89
+ catch {
90
+ // Treat as raw scene payload for backwards compatibility.
91
+ }
92
+ return { sceneData: raw };
93
+ }
94
+ function isSceneSyncMessage(value) {
95
+ if (typeof value !== "object" || value === null)
96
+ return false;
97
+ const obj = value;
98
+ return (obj.type === SCENE_SYNC_TYPE &&
99
+ typeof obj.sceneData === "string" &&
100
+ (obj.sceneId === undefined || typeof obj.sceneId === "string"));
101
+ }
102
+ function printEditorLink(sceneId, lastPrintedLink) {
103
+ const editorUrl = `${BASE_URL}/editor?sceneId=${encodeURIComponent(sceneId)}&wsEnabled=true`;
104
+ if (editorUrl === lastPrintedLink)
105
+ return lastPrintedLink;
106
+ console.log(chalk.cyan(` Editor: ${editorUrl}`));
107
+ return editorUrl;
108
+ }
109
+ function getRequiredManifestSceneId(sceneDir) {
110
+ const manifestPath = path.join(sceneDir, MANIFEST_FILE_NAME);
111
+ if (!fs.existsSync(manifestPath)) {
112
+ throw new Error(`Missing ${MANIFEST_FILE_NAME} in ${sceneDir}. Run "phibelle-kit clone" first or watch from a cloned scene folder.`);
113
+ }
114
+ let manifestRaw;
115
+ try {
116
+ manifestRaw = fs.readFileSync(manifestPath, "utf8");
117
+ }
118
+ catch {
119
+ throw new Error(`Failed to read ${MANIFEST_FILE_NAME} in ${sceneDir}.`);
120
+ }
121
+ let manifest;
122
+ try {
123
+ manifest = JSON.parse(manifestRaw);
124
+ }
125
+ catch {
126
+ throw new Error(`Invalid ${MANIFEST_FILE_NAME}: expected valid JSON.`);
127
+ }
128
+ if (typeof manifest.sceneId !== "string" || !manifest.sceneId.trim()) {
129
+ throw new Error(`Invalid ${MANIFEST_FILE_NAME}: missing required "sceneId".`);
98
130
  }
131
+ return manifest.sceneId.trim();
99
132
  }
@@ -0,0 +1,2 @@
1
+ /** WebSocket port for CLI ↔ app scene sync. Shared so the app and kit stay in sync. */
2
+ export declare const CLI_WS_PORT = 31113;
@@ -0,0 +1,2 @@
1
+ /** WebSocket port for CLI ↔ app scene sync. Shared so the app and kit stay in sync. */
2
+ export const CLI_WS_PORT = 31113;
package/dist/index.js CHANGED
@@ -1,30 +1,26 @@
1
1
  #!/usr/bin/env node
2
2
  import chalk from "chalk";
3
3
  import { watchSceneCommand } from "./commands/watch-scene.js";
4
+ import { initSceneCommand } from "./commands/init-scene.js";
4
5
  import { cloneSceneCommand } from "./commands/clone-scene.js";
5
- import { waitForToken } from "./lib/auth/wait-for-token.js";
6
- const USAGE = " Usage: phibelle-kit clone <sceneId> | phibelle-kit watch <sceneId>";
6
+ const USAGE = " Usage: phibelle-kit init | phibelle-kit watch | phibelle-kit clone";
7
7
  async function main() {
8
8
  const command = process.argv[2];
9
- const sceneId = process.argv[3];
10
- if (!command || !sceneId) {
11
- console.log(chalk.red(" Command and scene ID are required"));
12
- console.log(chalk.gray(USAGE));
13
- process.exit(1);
9
+ if (command === "init") {
10
+ initSceneCommand();
11
+ return;
14
12
  }
15
- await waitForToken();
16
- if (command === "clone") {
17
- await cloneSceneCommand(sceneId);
18
- process.exit(0);
19
- }
20
- else if (command === "watch") {
21
- await watchSceneCommand(sceneId);
13
+ if (command === "watch") {
14
+ await watchSceneCommand();
15
+ return;
22
16
  }
23
- else {
24
- console.log(chalk.red(` Unknown command: ${command}`));
25
- console.log(chalk.gray(USAGE));
26
- process.exit(1);
17
+ if (command === "clone") {
18
+ await cloneSceneCommand();
19
+ return;
27
20
  }
21
+ console.log(chalk.red(" Unknown or missing command"));
22
+ console.log(chalk.gray(USAGE));
23
+ process.exit(1);
28
24
  }
29
25
  main().catch((error) => {
30
26
  console.error(chalk.red("Error:"), error.message);
@@ -1,2 +1 @@
1
- export { createConvexClient, subscribeToQuery, queryOnce, mutateOnce } from "./convex-client.js";
2
- export { CONVEX_URL, BASE_URL } from "./public-env.js";
1
+ export { BASE_URL } from "./public-env.js";
@@ -1,2 +1 @@
1
- export { createConvexClient, subscribeToQuery, queryOnce, mutateOnce } from "./convex-client.js";
2
- export { CONVEX_URL, BASE_URL } from "./public-env.js";
1
+ export { BASE_URL } from "./public-env.js";
@@ -1,3 +1,2 @@
1
1
  export declare const isDev: boolean;
2
2
  export declare const BASE_URL: string;
3
- export declare const CONVEX_URL: string;
@@ -1,3 +1,2 @@
1
1
  export const isDev = process.env.NODE_ENV === "development";
2
2
  export const BASE_URL = isDev ? "http://localhost:3131" : "https://phibelle.studio";
3
- export const CONVEX_URL = isDev ? "https://mellow-toad-676.convex.cloud" : "https://keen-dove-500.convex.cloud";
@@ -1,8 +1,9 @@
1
1
  export declare const SCENE_APP_DIR = "app";
2
+ export declare const SCENE_FILE_NAME = "scene.phibelle";
2
3
  export declare function getSceneAppDir(sceneDir: string): string;
3
4
  export declare function folderExists(folderPath: string): boolean;
4
5
  export declare function createFolder(folderPath: string): void;
5
6
  /** Scene name to folder name: lowercased, spaces to hyphens, safe for filesystem. */
6
7
  export declare function sceneNameToFolder(name: string): string;
7
- /** Find directory containing scene.json with _id === sceneId: check cwd first, then subdirs. */
8
+ /** Find directory containing scene.phibelle with _id === sceneId: check cwd first, then subdirs. */
8
9
  export declare function findSceneDirBySceneId(cwd: string, sceneId: string): string | null;
@@ -1,6 +1,7 @@
1
1
  import * as path from "node:path";
2
2
  import * as fs from "fs";
3
3
  export const SCENE_APP_DIR = "app";
4
+ export const SCENE_FILE_NAME = "scene.phibelle";
4
5
  export function getSceneAppDir(sceneDir) {
5
6
  return path.join(sceneDir, SCENE_APP_DIR);
6
7
  }
@@ -21,9 +22,9 @@ export function sceneNameToFolder(name) {
21
22
  .replace(/-+/g, "-")
22
23
  .replace(/^-|-$/g, "") || "scene";
23
24
  }
24
- /** Find directory containing scene.json with _id === sceneId: check cwd first, then subdirs. */
25
+ /** Find directory containing scene.phibelle with _id === sceneId: check cwd first, then subdirs. */
25
26
  export function findSceneDirBySceneId(cwd, sceneId) {
26
- const scenePathInCwd = path.join(cwd, "scene.json");
27
+ const scenePathInCwd = path.join(cwd, SCENE_FILE_NAME);
27
28
  try {
28
29
  const raw = fs.readFileSync(scenePathInCwd, "utf8");
29
30
  const data = JSON.parse(raw);
@@ -43,7 +44,7 @@ export function findSceneDirBySceneId(cwd, sceneId) {
43
44
  for (const ent of entries) {
44
45
  if (!ent.isDirectory())
45
46
  continue;
46
- const scenePath = path.join(cwd, ent.name, "scene.json");
47
+ const scenePath = path.join(cwd, ent.name, SCENE_FILE_NAME);
47
48
  try {
48
49
  const raw = fs.readFileSync(scenePath, "utf8");
49
50
  const data = JSON.parse(raw);
@@ -51,7 +52,7 @@ export function findSceneDirBySceneId(cwd, sceneId) {
51
52
  return path.join(cwd, ent.name);
52
53
  }
53
54
  catch {
54
- // no scene.json or invalid
55
+ // no scene.phibelle or invalid
55
56
  }
56
57
  }
57
58
  return null;
@@ -1,7 +1,11 @@
1
1
  import { type ScriptWatcher } from "./script-watcher.js";
2
+ import { type WsServer } from "../ws/index.js";
2
3
  import type { Scene, WatchCallbacks } from "../types.js";
3
4
  /** Create scene dir, write scene.json, setup package.json/global.d.ts; log install hint if needed. */
4
5
  export declare function setupSceneFiles(scene: Scene, sceneDir: string, ignoreHint?: boolean): void;
5
6
  /** Unpackage scene scripts; log success or hint on failure. */
6
7
  export declare function unpackageWithLogging(sceneId: string, sceneDir: string, failHint: string): Promise<void>;
7
- export declare function runFirstUpdateSetup(scene: Scene, sceneDir: string, callbacks: WatchCallbacks): Promise<ScriptWatcher>;
8
+ export declare function runFirstUpdateSetup(scene: Scene, sceneDir: string, callbacks: WatchCallbacks, wsPort: number): Promise<{
9
+ scriptWatcher: ScriptWatcher;
10
+ wsServer: WsServer;
11
+ }>;
@@ -3,8 +3,10 @@ import * as path from "path";
3
3
  import * as fs from "fs";
4
4
  import { BASE_URL } from "../api/public-env.js";
5
5
  import { createFolder } from "./file-system.js";
6
- import { unpackageScene } from "./scene-packaging.js";
7
- import { createScriptWatcher } from "./script-watcher.js";
6
+ import { unpackageScene, getSceneFileContent } from "./scene-packaging.js";
7
+ import { createWsScriptWatcher } from "./script-watcher.js";
8
+ import { SceneState } from "./scene-state.js";
9
+ import { createWsServer } from "../ws/index.js";
8
10
  import { setupSceneDirectory, getInstallHint } from "../../scene-setup/setup.js";
9
11
  import { toErrorMessage } from "../utils.js";
10
12
  const INSTALL_HINT_LABEL = " ⚠ Install dependencies for types and intellisense:";
@@ -30,7 +32,7 @@ export async function unpackageWithLogging(sceneId, sceneDir, failHint) {
30
32
  console.log(chalk.gray(" " + failHint));
31
33
  }
32
34
  }
33
- export async function runFirstUpdateSetup(scene, sceneDir, callbacks) {
35
+ export async function runFirstUpdateSetup(scene, sceneDir, callbacks, wsPort) {
34
36
  setupSceneFiles(scene, sceneDir);
35
37
  console.log(chalk.yellow(" Opening folder: " + sceneDir));
36
38
  console.log();
@@ -41,5 +43,9 @@ export async function runFirstUpdateSetup(scene, sceneDir, callbacks) {
41
43
  console.log(chalk.white(` Last updated: ${new Date(scene.updatedAt).toLocaleString()}`));
42
44
  console.log(chalk.white(" Scene Link: ") + chalk.cyan(`${BASE_URL}/editor?sceneId=${scene._id}`));
43
45
  console.log();
44
- return createScriptWatcher(scene._id, sceneDir, callbacks.onPushed, callbacks.onNewEntity, callbacks.onError);
46
+ const initialJson = await getSceneFileContent(scene._id);
47
+ const sceneState = new SceneState(initialJson);
48
+ const wsServer = createWsServer(wsPort, { getSceneJson: () => sceneState.getJson() });
49
+ const scriptWatcher = createWsScriptWatcher(sceneState, sceneDir, wsServer, callbacks.onPushed, callbacks.onNewEntity, callbacks.onError);
50
+ return { scriptWatcher, wsServer };
45
51
  }
@@ -1,4 +1,3 @@
1
- export { setupSceneFiles, unpackageWithLogging, runFirstUpdateSetup } from "./first-run-setup.js";
2
1
  export { createScriptWatcher, type ScriptWatcher } from "./script-watcher.js";
3
2
  export { findSceneDirBySceneId, sceneNameToFolder, getSceneAppDir, createFolder, folderExists, SCENE_APP_DIR, } from "./file-system.js";
4
- export { unpackageScene, entityNameToSlug, slugToEntityName, ENTITY_FOLDER_RE, ENTITY_SCRIPT_FILE, ENTITY_PROPERTIES_FILE, ENTITY_TRANSFORMS_FILE, SCENE_SCRIPT_FILE, SCENE_PROPERTIES_FILE, } from "./scene-packaging.js";
3
+ export { unpackageScene, entityNameToSlug, slugToEntityName, ENTITY_SCRIPT_FILE, ENTITY_PROPERTIES_FILE, ENTITY_TRANSFORMS_FILE, SCENE_SCRIPT_FILE, SCENE_PROPERTIES_FILE, } from "./scene-packaging.js";
@@ -1,4 +1,3 @@
1
- export { setupSceneFiles, unpackageWithLogging, runFirstUpdateSetup } from "./first-run-setup.js";
2
1
  export { createScriptWatcher } from "./script-watcher.js";
3
2
  export { findSceneDirBySceneId, sceneNameToFolder, getSceneAppDir, createFolder, folderExists, SCENE_APP_DIR, } from "./file-system.js";
4
- export { unpackageScene, entityNameToSlug, slugToEntityName, ENTITY_FOLDER_RE, ENTITY_SCRIPT_FILE, ENTITY_PROPERTIES_FILE, ENTITY_TRANSFORMS_FILE, SCENE_SCRIPT_FILE, SCENE_PROPERTIES_FILE, } from "./scene-packaging.js";
3
+ export { unpackageScene, entityNameToSlug, slugToEntityName, ENTITY_SCRIPT_FILE, ENTITY_PROPERTIES_FILE, ENTITY_TRANSFORMS_FILE, SCENE_SCRIPT_FILE, SCENE_PROPERTIES_FILE, } from "./scene-packaging.js";
@@ -1,26 +1,20 @@
1
+ import type { PhibelleScene } from "../types.js";
1
2
  /** Scene-level file names in app/. */
2
3
  export declare const SCENE_SCRIPT_FILE = "scene-script.tsx";
3
4
  export declare const SCENE_PROPERTIES_FILE = "scene-properties.json";
5
+ export declare function parseSceneJson(content: string): PhibelleScene;
4
6
  /** Convert entity display name to slug: "Main Enemy" → "main-enemy". */
5
7
  export declare function entityNameToSlug(name: string): string;
6
8
  /** Convert slug to display name: "main-enemy" → "Main Enemy". */
7
9
  export declare function slugToEntityName(slug: string): string;
8
- /**
9
- * Matches entity folder names: <entity-name>-<engineId> (e.g. main-enemy-1).
10
- * Group 1 = slug, group 2 = engine id.
11
- */
12
- export declare const ENTITY_FOLDER_RE: RegExp;
13
10
  /** Entity filenames inside an entity folder. */
14
11
  export declare const ENTITY_SCRIPT_FILE = "script.tsx";
15
12
  export declare const ENTITY_PROPERTIES_FILE = "properties.json";
16
13
  export declare const ENTITY_TRANSFORMS_FILE = "transforms.json";
17
- export declare function unpackageScene(sceneId: string, sceneDir: string): Promise<void>;
18
- export declare function pushScriptChange(sceneId: string, sceneDir: string, changedFile: string): Promise<void>;
19
- /** Push entity properties from app/<folder>/properties.json to the scene. */
20
- export declare function pushPropertiesChange(sceneId: string, sceneDir: string, relativePath: string): Promise<void>;
21
- /** Push scene properties from app/scene-properties.json to the scene. */
22
- export declare function pushScenePropertiesChange(sceneId: string, sceneDir: string): Promise<void>;
23
- /** Push entity transform from app/<folder>/transforms.json to the scene. */
24
- export declare function pushTransformsChange(sceneId: string, sceneDir: string, relativePath: string): Promise<void>;
25
- /** Add a new entity to the scene when user creates <entity-name>-<engineId>/ with script.tsx (and optional properties.json, transforms.json). */
26
- export declare function pushNewEntity(sceneId: string, sceneDir: string, engineId: number, folderName: string): Promise<void>;
14
+ /** Unpackage scene JSON to filesystem. No DB; accepts full scene JSON string. */
15
+ export declare function unpackageScene(sceneJson: string, sceneDir: string, sceneId?: string): Promise<void>;
16
+ /**
17
+ * Apply a single file change to the in-memory scene and return updated JSON.
18
+ * Used by script watcher to produce scene snapshot to send to FE.
19
+ */
20
+ export declare function patchSceneFromFile(sceneJson: string, sceneDir: string, relativePath: string, eventType: "change" | "add"): Promise<string>;