phibelle-kit 1.0.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.

Potentially problematic release.


This version of phibelle-kit might be problematic. Click here for more details.

Files changed (91) hide show
  1. package/.vscode/settings.json +22 -0
  2. package/README.md +27 -0
  3. package/convex/_generated/api.d.ts +53 -0
  4. package/convex/_generated/api.js +23 -0
  5. package/convex/_generated/dataModel.d.ts +60 -0
  6. package/convex/_generated/server.d.ts +143 -0
  7. package/convex/_generated/server.js +93 -0
  8. package/dist/clone-scene.d.ts +5 -0
  9. package/dist/clone-scene.js +39 -0
  10. package/dist/commands/clone-scene.d.ts +5 -0
  11. package/dist/commands/clone-scene.js +39 -0
  12. package/dist/commands/hello.d.ts +1 -0
  13. package/dist/commands/hello.js +10 -0
  14. package/dist/commands/login.d.ts +1 -0
  15. package/dist/commands/login.js +75 -0
  16. package/dist/commands/logout.d.ts +1 -0
  17. package/dist/commands/logout.js +16 -0
  18. package/dist/commands/watch-scene.d.ts +1 -0
  19. package/dist/commands/watch-scene.js +103 -0
  20. package/dist/constants.d.ts +4 -0
  21. package/dist/constants.js +6 -0
  22. package/dist/convex/_generated/api.d.ts +11 -0
  23. package/dist/convex/_generated/api.js +21 -0
  24. package/dist/convex/_generated/server.d.ts +67 -0
  25. package/dist/convex/_generated/server.js +77 -0
  26. package/dist/index.d.ts +2 -0
  27. package/dist/index.js +32 -0
  28. package/dist/lib/conf.d.ts +5 -0
  29. package/dist/lib/conf.js +23 -0
  30. package/dist/lib/constants.d.ts +4 -0
  31. package/dist/lib/constants.js +6 -0
  32. package/dist/lib/convex-client.d.ts +47 -0
  33. package/dist/lib/convex-client.js +80 -0
  34. package/dist/lib/file-system.d.ts +6 -0
  35. package/dist/lib/file-system.js +54 -0
  36. package/dist/lib/first-run-setup.d.ts +12 -0
  37. package/dist/lib/first-run-setup.js +45 -0
  38. package/dist/lib/folder.d.ts +3 -0
  39. package/dist/lib/folder.js +33 -0
  40. package/dist/lib/public-env.d.ts +3 -0
  41. package/dist/lib/public-env.js +3 -0
  42. package/dist/lib/scene-packaging.d.ts +6 -0
  43. package/dist/lib/scene-packaging.js +196 -0
  44. package/dist/lib/script-watcher.d.ts +9 -0
  45. package/dist/lib/script-watcher.js +90 -0
  46. package/dist/lib/types.d.ts +62 -0
  47. package/dist/lib/types.js +1 -0
  48. package/dist/lib/utils.d.ts +1 -0
  49. package/dist/lib/utils.js +3 -0
  50. package/dist/lib/wait-for-token.d.ts +1 -0
  51. package/dist/lib/wait-for-token.js +30 -0
  52. package/dist/package/global-module.d.ts +1 -0
  53. package/dist/package/global-module.js +117 -0
  54. package/dist/package/npm-package.d.ts +1 -0
  55. package/dist/package/npm-package.js +16 -0
  56. package/dist/package/phibelle-module.d.ts +1 -0
  57. package/dist/package/phibelle-module.js +122 -0
  58. package/dist/package/setup.d.ts +4 -0
  59. package/dist/package/setup.js +41 -0
  60. package/dist/scene-setup/global-module.d.ts +1 -0
  61. package/dist/scene-setup/global-module.js +117 -0
  62. package/dist/scene-setup/npm-package.d.ts +1 -0
  63. package/dist/scene-setup/npm-package.js +13 -0
  64. package/dist/scene-setup/setup.d.ts +15 -0
  65. package/dist/scene-setup/setup.js +51 -0
  66. package/dist/watch-scene.d.ts +1 -0
  67. package/dist/watch-scene.js +103 -0
  68. package/package.json +44 -0
  69. package/src/commands/clone-scene.ts +46 -0
  70. package/src/commands/watch-scene.ts +117 -0
  71. package/src/convex/_generated/README.md +0 -0
  72. package/src/convex/_generated/api.d.ts +53 -0
  73. package/src/convex/_generated/api.js +23 -0
  74. package/src/convex/_generated/dataModel.d.ts +60 -0
  75. package/src/convex/_generated/server.d.ts +143 -0
  76. package/src/convex/_generated/server.js +93 -0
  77. package/src/index.ts +37 -0
  78. package/src/lib/conf.ts +28 -0
  79. package/src/lib/convex-client.ts +101 -0
  80. package/src/lib/file-system.ts +53 -0
  81. package/src/lib/first-run-setup.ts +73 -0
  82. package/src/lib/public-env.ts +3 -0
  83. package/src/lib/scene-packaging.ts +238 -0
  84. package/src/lib/script-watcher.ts +103 -0
  85. package/src/lib/types.ts +63 -0
  86. package/src/lib/utils.ts +3 -0
  87. package/src/lib/wait-for-token.ts +34 -0
  88. package/src/package/global-module.ts +117 -0
  89. package/src/package/npm-package.ts +16 -0
  90. package/src/package/setup.ts +47 -0
  91. package/tsconfig.json +19 -0
@@ -0,0 +1,33 @@
1
+ import { spawn } from "node:child_process";
2
+ import os from "node:os";
3
+ import * as fs from "fs";
4
+ export function openFolder(folderPath) {
5
+ const platform = os.platform();
6
+ let command;
7
+ let args;
8
+ if (platform === "win32") {
9
+ command = "explorer.exe";
10
+ args = [folderPath];
11
+ }
12
+ else if (platform === "darwin") {
13
+ command = "open";
14
+ args = [folderPath];
15
+ }
16
+ else {
17
+ command = "xdg-open";
18
+ args = [folderPath];
19
+ }
20
+ const child = spawn(command, args, {
21
+ detached: true,
22
+ stdio: "ignore",
23
+ });
24
+ child.unref();
25
+ }
26
+ export function folderExists(folderPath) {
27
+ return fs.existsSync(folderPath);
28
+ }
29
+ export function createFolder(folderPath) {
30
+ if (!folderExists(folderPath)) {
31
+ fs.mkdirSync(folderPath, { recursive: true });
32
+ }
33
+ }
@@ -0,0 +1,3 @@
1
+ export declare const isDev: boolean;
2
+ export declare const BASE_URL: string;
3
+ export declare const CONVEX_URL: string;
@@ -0,0 +1,3 @@
1
+ export const isDev = process.env.NODE_ENV === "development";
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";
@@ -0,0 +1,6 @@
1
+ /** Matches entity script filenames; group 1 is engine id. Use in both .test() and .match(). */
2
+ export declare const ENTITY_FILE_RE: RegExp;
3
+ export declare function unpackageScene(sceneId: string, sceneDir: string): Promise<void>;
4
+ export declare function pushScriptChange(sceneId: string, sceneDir: string, changedFile: string): Promise<void>;
5
+ /** Add a new entity to the scene when user creates entity-<engineId>.tsx locally. */
6
+ export declare function pushNewEntity(sceneId: string, sceneDir: string, engineId: number): Promise<void>;
@@ -0,0 +1,196 @@
1
+ import * as fs from "fs/promises";
2
+ import * as path from "path";
3
+ import { queryOnce, mutateOnce } from "./convex-client.js";
4
+ import { api } from "../convex/_generated/api.js";
5
+ import { getSessionId } from "./conf.js";
6
+ async function getSceneFileContent(sceneId) {
7
+ const fileUrl = await queryOnce(api.scenes.getSceneFileUrl, { sceneId });
8
+ if (fileUrl == null) {
9
+ throw new Error("Scene file not found (scene may be missing or have no file yet)");
10
+ }
11
+ const response = await fetch(fileUrl);
12
+ if (!response.ok) {
13
+ throw new Error(`Failed to download scene file: ${response.status} ${response.statusText}`);
14
+ }
15
+ return response.text();
16
+ }
17
+ function parseSceneJson(content) {
18
+ try {
19
+ return JSON.parse(content);
20
+ }
21
+ catch {
22
+ throw new Error("Invalid scene file: not valid JSON");
23
+ }
24
+ }
25
+ /** Matches entity script filenames; group 1 is engine id. Use in both .test() and .match(). */
26
+ export const ENTITY_FILE_RE = /^entity-(\d+)\.tsx$/;
27
+ async function uploadSceneJsonAndUpdate(sceneId, json) {
28
+ const uploadUrl = await mutateOnce(api.storage.generateUploadUrl, {});
29
+ if (typeof uploadUrl !== "string") {
30
+ throw new Error("Invalid upload URL response");
31
+ }
32
+ const uploadResult = await fetch(uploadUrl, {
33
+ method: "POST",
34
+ headers: { "Content-Type": "application/json" },
35
+ body: json,
36
+ });
37
+ if (!uploadResult.ok) {
38
+ throw new Error(`Failed to upload scene file: ${uploadResult.status} ${uploadResult.statusText}`);
39
+ }
40
+ const uploadResponse = (await uploadResult.json());
41
+ const storageId = uploadResponse.storageId;
42
+ if (!storageId) {
43
+ throw new Error("Upload response missing storageId");
44
+ }
45
+ const sceneByteLength = Buffer.byteLength(json, "utf8");
46
+ await mutateOnce(api.scenes.update, {
47
+ id: sceneId,
48
+ sceneFileId: storageId,
49
+ sceneByteLength,
50
+ lastModifiedSessionId: getSessionId(),
51
+ });
52
+ }
53
+ export async function unpackageScene(sceneId, sceneDir) {
54
+ const content = await getSceneFileContent(sceneId);
55
+ const parsed = parseSceneJson(content);
56
+ await fs.mkdir(sceneDir, { recursive: true });
57
+ // Remove existing entity files so entities deleted in the editor are removed from disk
58
+ try {
59
+ const entries = await fs.readdir(sceneDir, { withFileTypes: true });
60
+ for (const ent of entries) {
61
+ if (ent.isFile() && ENTITY_FILE_RE.test(ent.name)) {
62
+ await fs.unlink(path.join(sceneDir, ent.name));
63
+ }
64
+ }
65
+ }
66
+ catch {
67
+ // Directory might not exist or be empty; ignore
68
+ }
69
+ const entities = parsed.entityStore?.entities ?? {};
70
+ const manifest = {
71
+ sceneScript: "scene.tsx",
72
+ entities: {},
73
+ };
74
+ await fs.writeFile(path.join(sceneDir, "scene.tsx"), parsed.sceneScriptStore?.sceneScript ?? "", "utf8");
75
+ for (const idStr of Object.keys(entities)) {
76
+ const entity = entities[idStr];
77
+ if (!entity || typeof entity.script !== "string")
78
+ continue;
79
+ const engineId = String(entity.engineId ?? idStr);
80
+ const fileName = `entity-${engineId}.tsx`;
81
+ manifest.entities[engineId] = fileName;
82
+ await fs.writeFile(path.join(sceneDir, fileName), entity.script, "utf8");
83
+ }
84
+ await fs.writeFile(path.join(sceneDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
85
+ }
86
+ export async function pushScriptChange(sceneId, sceneDir, changedFile) {
87
+ const manifestPath = path.join(sceneDir, "manifest.json");
88
+ let manifestRaw;
89
+ try {
90
+ manifestRaw = await fs.readFile(manifestPath, "utf8");
91
+ }
92
+ catch {
93
+ throw new Error("manifest.json not found; run watch-scene to unpackage first");
94
+ }
95
+ const manifest = JSON.parse(manifestRaw);
96
+ const content = await getSceneFileContent(sceneId);
97
+ const parsed = parseSceneJson(content);
98
+ const filePath = path.join(sceneDir, changedFile);
99
+ let fileContent;
100
+ try {
101
+ fileContent = await fs.readFile(filePath, "utf8");
102
+ }
103
+ catch {
104
+ throw new Error(`Could not read file: ${changedFile}`);
105
+ }
106
+ const isSceneScript = manifest.sceneScript === changedFile;
107
+ if (isSceneScript) {
108
+ if (!parsed.sceneScriptStore)
109
+ parsed.sceneScriptStore = { sceneScript: "", sceneProperties: [], lastModifiedSceneScript: 0 };
110
+ parsed.sceneScriptStore.sceneScript = fileContent;
111
+ if (typeof parsed.sceneScriptStore.lastModifiedSceneScript === "number") {
112
+ parsed.sceneScriptStore.lastModifiedSceneScript = Date.now();
113
+ }
114
+ }
115
+ else {
116
+ const entityId = Object.entries(manifest.entities).find(([, f]) => f === changedFile)?.[0];
117
+ if (entityId == null) {
118
+ throw new Error(`Unknown script file: ${changedFile}`);
119
+ }
120
+ const entities = parsed.entityStore?.entities ?? {};
121
+ const entity = entities[entityId];
122
+ if (!entity) {
123
+ throw new Error(`Entity ${entityId} no longer in scene; skip or sync from app`);
124
+ }
125
+ entity.script = fileContent;
126
+ entity.scriptVersion = (entity.scriptVersion ?? 0) + 1;
127
+ }
128
+ const json = JSON.stringify(parsed);
129
+ await uploadSceneJsonAndUpdate(sceneId, json);
130
+ }
131
+ const DEFAULT_POSITION = '{"x":0,"y":0,"z":0}';
132
+ const DEFAULT_ROTATION = '{"isEuler":true,"_x":0,"_y":0,"_z":0,"_order":"XYZ"}';
133
+ const DEFAULT_SCALE = '{"x":1,"y":1,"z":1}';
134
+ /** Add a new entity to the scene when user creates entity-<engineId>.tsx locally. */
135
+ export async function pushNewEntity(sceneId, sceneDir, engineId) {
136
+ const manifestPath = path.join(sceneDir, "manifest.json");
137
+ let manifestRaw;
138
+ try {
139
+ manifestRaw = await fs.readFile(manifestPath, "utf8");
140
+ }
141
+ catch {
142
+ throw new Error("manifest.json not found; run watch-scene to unpackage first");
143
+ }
144
+ const manifest = JSON.parse(manifestRaw);
145
+ const idStr = String(engineId);
146
+ if (manifest.entities[idStr]) {
147
+ throw new Error(`Entity ${engineId} already exists in manifest`);
148
+ }
149
+ const content = await getSceneFileContent(sceneId);
150
+ const parsed = parseSceneJson(content);
151
+ const fileName = `entity-${engineId}.tsx`;
152
+ const filePath = path.join(sceneDir, fileName);
153
+ let script;
154
+ try {
155
+ script = await fs.readFile(filePath, "utf8");
156
+ }
157
+ catch {
158
+ throw new Error(`Could not read file: ${fileName}`);
159
+ }
160
+ if (!parsed.entityStore) {
161
+ parsed.entityStore = {
162
+ _currId: 0,
163
+ rootEntities: [],
164
+ entities: {},
165
+ rootRenderVersion: 0,
166
+ lastRenderedVersion: 0,
167
+ selectedEntityIds: [],
168
+ };
169
+ }
170
+ const store = parsed.entityStore;
171
+ store._currId = Math.max(store._currId, engineId);
172
+ if (!store.rootEntities.includes(engineId)) {
173
+ store.rootEntities.push(engineId);
174
+ }
175
+ store.entities[idStr] = {
176
+ name: `Entity-${engineId}`,
177
+ engineId,
178
+ childrenIdsSet: [],
179
+ childrenIds: [],
180
+ isNodeOpen: false,
181
+ script,
182
+ scriptVersion: 0,
183
+ transform: {
184
+ position: DEFAULT_POSITION,
185
+ rotation: DEFAULT_ROTATION,
186
+ scale: DEFAULT_SCALE,
187
+ },
188
+ renderVersion: 0,
189
+ properties: [],
190
+ threeId: 0,
191
+ };
192
+ manifest.entities[idStr] = fileName;
193
+ await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf8");
194
+ const json = JSON.stringify(parsed);
195
+ await uploadSceneJsonAndUpdate(sceneId, json);
196
+ }
@@ -0,0 +1,9 @@
1
+ export type ScriptWatcher = {
2
+ close: () => void;
3
+ };
4
+ export type WatchCallbacks = {
5
+ onPushed: (file: string) => void;
6
+ onNewEntity: (engineId: number) => void;
7
+ onError: (file: string, err: Error) => void;
8
+ };
9
+ export declare function createScriptWatcher(sceneId: string, sceneDir: string, onPushed: (file: string) => void, onNewEntity: (engineId: number) => void, onError: (file: string, err: Error) => void): ScriptWatcher;
@@ -0,0 +1,90 @@
1
+ import * as path from "path";
2
+ import * as fs from "fs";
3
+ import chokidar from "chokidar";
4
+ import { pushScriptChange, pushNewEntity, ENTITY_FILE_RE } from "./scene-packaging.js";
5
+ const DEBOUNCE_MS = 600;
6
+ export function createScriptWatcher(sceneId, sceneDir, onPushed, onNewEntity, onError) {
7
+ const manifestPath = path.join(sceneDir, "manifest.json");
8
+ function readManifest() {
9
+ try {
10
+ const raw = fs.readFileSync(manifestPath, "utf8");
11
+ return JSON.parse(raw);
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
17
+ let debounceTimer = null;
18
+ let lastEvent = null;
19
+ function schedule(type, file) {
20
+ lastEvent = { type, file };
21
+ if (debounceTimer)
22
+ clearTimeout(debounceTimer);
23
+ debounceTimer = setTimeout(async () => {
24
+ debounceTimer = null;
25
+ const ev = lastEvent;
26
+ lastEvent = null;
27
+ if (!ev)
28
+ return;
29
+ const manifest = readManifest();
30
+ if (!manifest)
31
+ return;
32
+ const basename = ev.file;
33
+ if (basename === manifest.sceneScript) {
34
+ try {
35
+ await pushScriptChange(sceneId, sceneDir, basename);
36
+ onPushed(basename);
37
+ }
38
+ catch (err) {
39
+ onError(basename, err instanceof Error ? err : new Error(String(err)));
40
+ }
41
+ return;
42
+ }
43
+ const entityMatch = basename.match(ENTITY_FILE_RE);
44
+ if (entityMatch) {
45
+ const engineId = parseInt(entityMatch[1], 10);
46
+ const isNew = !manifest.entities[String(engineId)];
47
+ if (isNew) {
48
+ try {
49
+ await pushNewEntity(sceneId, sceneDir, engineId);
50
+ onNewEntity(engineId);
51
+ }
52
+ catch (err) {
53
+ onError(basename, err instanceof Error ? err : new Error(String(err)));
54
+ }
55
+ }
56
+ else {
57
+ try {
58
+ await pushScriptChange(sceneId, sceneDir, basename);
59
+ onPushed(basename);
60
+ }
61
+ catch (err) {
62
+ onError(basename, err instanceof Error ? err : new Error(String(err)));
63
+ }
64
+ }
65
+ }
66
+ }, DEBOUNCE_MS);
67
+ }
68
+ const watcher = chokidar.watch(sceneDir, { ignoreInitial: true });
69
+ watcher.on("change", (filePath) => {
70
+ const basename = path.basename(filePath);
71
+ if (basename === "manifest.json")
72
+ return;
73
+ if (basename === "scene.tsx" || ENTITY_FILE_RE.test(basename)) {
74
+ schedule("change", basename);
75
+ }
76
+ });
77
+ watcher.on("add", (filePath) => {
78
+ const basename = path.basename(filePath);
79
+ if (basename === "scene.tsx" || ENTITY_FILE_RE.test(basename)) {
80
+ schedule("add", basename);
81
+ }
82
+ });
83
+ return {
84
+ close: () => {
85
+ if (debounceTimer)
86
+ clearTimeout(debounceTimer);
87
+ watcher.close();
88
+ },
89
+ };
90
+ }
@@ -0,0 +1,62 @@
1
+ /** Scene document from Convex (scenes.getById / listMyScenes) */
2
+ export type Scene = {
3
+ _id: string;
4
+ name: string;
5
+ updatedAt: number;
6
+ ownerId: string;
7
+ sceneFileId: string;
8
+ createdAt: number;
9
+ sceneByteLength?: number;
10
+ screenshotFileId?: string;
11
+ /** Set when scene file is updated; used to avoid re-unpack when this CLI saved. */
12
+ lastModifiedSessionId?: string;
13
+ };
14
+ export type PhibelleScene = {
15
+ entityStore: {
16
+ _currId: number;
17
+ rootEntities: number[];
18
+ entities: {
19
+ [id: string]: {
20
+ name: string;
21
+ engineId: number;
22
+ childrenIdsSet: number[];
23
+ childrenIds: number[];
24
+ isNodeOpen: boolean;
25
+ script: string;
26
+ scriptVersion: number;
27
+ transform: {
28
+ position: string;
29
+ rotation: string;
30
+ scale: string;
31
+ };
32
+ renderVersion: number;
33
+ properties: Array<{
34
+ name: string;
35
+ type: string;
36
+ value: any;
37
+ }>;
38
+ threeId: number;
39
+ };
40
+ };
41
+ rootRenderVersion: number;
42
+ lastRenderedVersion: number;
43
+ selectedEntityIds: number[];
44
+ };
45
+ editorCameraStore: {
46
+ orthographic: boolean;
47
+ cameraPosition: [number, number, number];
48
+ cameraRotation: [number, number, number, string];
49
+ targetPosition: [number, number, number];
50
+ };
51
+ errorStore: Record<string, unknown>;
52
+ sceneScriptStore: {
53
+ sceneScript: string;
54
+ sceneProperties: any[];
55
+ lastModifiedSceneScript: number;
56
+ };
57
+ engineModeStore: {
58
+ engineMode: string;
59
+ transformMode: string;
60
+ };
61
+ version: string;
62
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export declare function toErrorMessage(err: unknown): string;
@@ -0,0 +1,3 @@
1
+ export function toErrorMessage(err) {
2
+ return err instanceof Error ? err.message : String(err);
3
+ }
@@ -0,0 +1 @@
1
+ export declare function waitForToken(): Promise<string>;
@@ -0,0 +1,30 @@
1
+ import http from "node:http";
2
+ import { setToken } from "./conf.js";
3
+ import { BASE_URL } from "./public-env.js";
4
+ import open from "open";
5
+ const PORT = 13131;
6
+ export function waitForToken() {
7
+ return new Promise((resolve) => {
8
+ const server = http.createServer((req, res) => {
9
+ const url = new URL(req.url, `http://localhost`);
10
+ const token = url.searchParams.get("token");
11
+ res.setHeader("Access-Control-Allow-Origin", "*");
12
+ res.setHeader("Access-Control-Allow-Methods", "GET");
13
+ if (token) {
14
+ res.writeHead(200, { "Content-Type": "text/html" });
15
+ res.end("Token received");
16
+ server.close();
17
+ resolve(token);
18
+ setToken(token);
19
+ }
20
+ else {
21
+ res.writeHead(400);
22
+ res.end("Missing token");
23
+ }
24
+ });
25
+ server.listen(PORT, () => {
26
+ console.log("Opening browser to authenticate...");
27
+ open(`${BASE_URL}/cli-token`);
28
+ });
29
+ });
30
+ }
@@ -0,0 +1 @@
1
+ export declare const GLOBAL_MODULE = "\nimport type React from \"react\";\nimport type * as THREE_IMPORT from \"three\";\nimport type * as DREI_IMPORT from \"@react-three/drei\";\nimport type * as R3F_IMPORT from \"@react-three/fiber\";\nimport type * as UIKIT_IMPORT from \"@react-three/uikit\";\nimport type * as RAPIER_IMPORT from \"@react-three/rapier\";\n\ndeclare global {\n const THREE: typeof THREE_IMPORT;\n const DREI: typeof DREI_IMPORT;\n const R3F: typeof R3F_IMPORT;\n const UIKIT: typeof UIKIT_IMPORT;\n const RAPIER: typeof RAPIER_IMPORT;\n\n namespace PHI {\n type Transform = {\n position: THREE_IMPORT.Vector3;\n rotation: THREE_IMPORT.Euler;\n scale: THREE_IMPORT.Vector3;\n };\n\n type PropertyType = string;\n\n type Property = {\n name: string;\n type: PropertyType;\n value: unknown;\n };\n\n type EntityData = {\n name: string;\n engineId: number;\n threeId?: number;\n parentId?: number;\n childrenIds: number[];\n transform: Transform;\n properties: Property[];\n };\n\n type GamepadInfo = {\n connected: boolean;\n buttonA: boolean;\n buttonB: boolean;\n buttonX: boolean;\n buttonY: boolean;\n joystick: [number, number];\n joystickRight: [number, number];\n RB: boolean;\n LB: boolean;\n RT: boolean;\n LT: boolean;\n start: boolean;\n select: boolean;\n up: boolean;\n down: boolean;\n left: boolean;\n right: boolean;\n };\n\n type EngineMode = {\n editMode: boolean;\n playMode: boolean;\n };\n\n type SceneScriptState = {\n sceneProperties: Record<string, unknown>;\n };\n\n type EntityRenderRegistry = Record<\n string,\n React.FC<{ entityData: EntityData }>\n >;\n }\n\n const PHI: {\n globalStore: Record<string, any>;\n\n PROPERTY_TYPES: ReadonlyArray<{\n type: PHI.PropertyType;\n defaultValue: unknown;\n }>;\n\n phibelleResetSelectedEntityIds(): void;\n\n useEntityThreeObject(\n entityData: PHI.EntityData,\n threeScene: THREE_IMPORT.Scene\n ): THREE_IMPORT.Object3D | null;\n\n useGamepad(): PHI.GamepadInfo;\n\n usePlayModeFrame(\n callback: (state: R3F_IMPORT.RootState, delta: number) => void\n ): void;\n\n useEngineMode(): PHI.EngineMode;\n\n useSceneScriptStore(): PHI.SceneScriptState;\n useSceneScriptStore<T>(\n selector: (state: PHI.SceneScriptState) => T\n ): T;\n\n editEntities(\n edits: Array<{ id: number; newData: Partial<PHI.EntityData> }>\n ): void;\n\n useEntity(entityId: number): { entityData: PHI.EntityData };\n\n PhibelleRoot: React.FC<{\n entityRenderRegistry: PHI.EntityRenderRegistry;\n }>;\n };\n}\n\nexport {};\n";
@@ -0,0 +1,117 @@
1
+ export const GLOBAL_MODULE = `
2
+ import type React from "react";
3
+ import type * as THREE_IMPORT from "three";
4
+ import type * as DREI_IMPORT from "@react-three/drei";
5
+ import type * as R3F_IMPORT from "@react-three/fiber";
6
+ import type * as UIKIT_IMPORT from "@react-three/uikit";
7
+ import type * as RAPIER_IMPORT from "@react-three/rapier";
8
+
9
+ declare global {
10
+ const THREE: typeof THREE_IMPORT;
11
+ const DREI: typeof DREI_IMPORT;
12
+ const R3F: typeof R3F_IMPORT;
13
+ const UIKIT: typeof UIKIT_IMPORT;
14
+ const RAPIER: typeof RAPIER_IMPORT;
15
+
16
+ namespace PHI {
17
+ type Transform = {
18
+ position: THREE_IMPORT.Vector3;
19
+ rotation: THREE_IMPORT.Euler;
20
+ scale: THREE_IMPORT.Vector3;
21
+ };
22
+
23
+ type PropertyType = string;
24
+
25
+ type Property = {
26
+ name: string;
27
+ type: PropertyType;
28
+ value: unknown;
29
+ };
30
+
31
+ type EntityData = {
32
+ name: string;
33
+ engineId: number;
34
+ threeId?: number;
35
+ parentId?: number;
36
+ childrenIds: number[];
37
+ transform: Transform;
38
+ properties: Property[];
39
+ };
40
+
41
+ type GamepadInfo = {
42
+ connected: boolean;
43
+ buttonA: boolean;
44
+ buttonB: boolean;
45
+ buttonX: boolean;
46
+ buttonY: boolean;
47
+ joystick: [number, number];
48
+ joystickRight: [number, number];
49
+ RB: boolean;
50
+ LB: boolean;
51
+ RT: boolean;
52
+ LT: boolean;
53
+ start: boolean;
54
+ select: boolean;
55
+ up: boolean;
56
+ down: boolean;
57
+ left: boolean;
58
+ right: boolean;
59
+ };
60
+
61
+ type EngineMode = {
62
+ editMode: boolean;
63
+ playMode: boolean;
64
+ };
65
+
66
+ type SceneScriptState = {
67
+ sceneProperties: Record<string, unknown>;
68
+ };
69
+
70
+ type EntityRenderRegistry = Record<
71
+ string,
72
+ React.FC<{ entityData: EntityData }>
73
+ >;
74
+ }
75
+
76
+ const PHI: {
77
+ globalStore: Record<string, any>;
78
+
79
+ PROPERTY_TYPES: ReadonlyArray<{
80
+ type: PHI.PropertyType;
81
+ defaultValue: unknown;
82
+ }>;
83
+
84
+ phibelleResetSelectedEntityIds(): void;
85
+
86
+ useEntityThreeObject(
87
+ entityData: PHI.EntityData,
88
+ threeScene: THREE_IMPORT.Scene
89
+ ): THREE_IMPORT.Object3D | null;
90
+
91
+ useGamepad(): PHI.GamepadInfo;
92
+
93
+ usePlayModeFrame(
94
+ callback: (state: R3F_IMPORT.RootState, delta: number) => void
95
+ ): void;
96
+
97
+ useEngineMode(): PHI.EngineMode;
98
+
99
+ useSceneScriptStore(): PHI.SceneScriptState;
100
+ useSceneScriptStore<T>(
101
+ selector: (state: PHI.SceneScriptState) => T
102
+ ): T;
103
+
104
+ editEntities(
105
+ edits: Array<{ id: number; newData: Partial<PHI.EntityData> }>
106
+ ): void;
107
+
108
+ useEntity(entityId: number): { entityData: PHI.EntityData };
109
+
110
+ PhibelleRoot: React.FC<{
111
+ entityRenderRegistry: PHI.EntityRenderRegistry;
112
+ }>;
113
+ };
114
+ }
115
+
116
+ export {};
117
+ `;
@@ -0,0 +1 @@
1
+ export declare const getNpmPackage: (sceneId: string) => string;
@@ -0,0 +1,16 @@
1
+ export const getNpmPackage = (sceneId) => {
2
+ return `{
3
+ "scripts": {
4
+ "watch": "phibelle-kit watch ${sceneId}"
5
+ },
6
+ "dependencies": {
7
+ "@react-three/drei": "^10.7.7",
8
+ "@react-three/fiber": "^9.5.0",
9
+ "@react-three/rapier": "^2.2.0",
10
+ "@react-three/uikit": "^1.0.61",
11
+ "react": "^19.1.1",
12
+ "react-dom": "19.1.1",
13
+ "three": "^0.179.1"
14
+ }
15
+ }`;
16
+ };
@@ -0,0 +1 @@
1
+ export declare const PHIBELLE_MODULE = "\n// node_modules/phibelle/index.d.ts\nimport { RootState } from \"@react-three/fiber\";\nimport type * as THREE from \"three\";\n\ndeclare module \"phibelle\" {\n // ----------------------------\n // Global window store\n // ----------------------------\n export const globalStore: {\n [key: string]: any;\n };\n\n // ----------------------------\n // Types used by scripts\n // ----------------------------\n export type Transform = {\n position: THREE.Vector3;\n rotation: THREE.Euler;\n scale: THREE.Vector3;\n };\n\n export type PropertyType = string;\n\n export type Property = {\n name: string;\n type: PropertyType;\n value: string | number | boolean | [number, number, number];\n };\n\n export type EntityData = {\n name: string;\n engineId: number;\n threeId?: number;\n parentId?: number;\n childrenIds: number[];\n transform: Transform;\n properties: Property[];\n };\n\n export const PROPERTY_TYPES: ReadonlyArray<{\n type: PropertyType;\n defaultValue: unknown;\n }>;\n\n // ----------------------------\n // Engine / editor functions\n // ----------------------------\n export function phibelleResetSelectedEntityIds(): void;\n\n // ----------------------------\n // Hooks exposed to scripts\n // ----------------------------\n export function useEntityThreeObject(\n entityData: EntityData,\n threeScene: THREE.Scene\n ): THREE.Object3D | null;\n\n export type GamepadInfo = {\n connected: boolean;\n buttonA: boolean;\n buttonB: boolean;\n buttonX: boolean;\n buttonY: boolean;\n joystick: [number, number];\n joystickRight: [number, number];\n RB: boolean;\n LB: boolean;\n RT: boolean;\n LT: boolean;\n start: boolean;\n select: boolean;\n up: boolean;\n down: boolean;\n left: boolean;\n right: boolean;\n };\n\n export function useGamepad(): GamepadInfo;\n\n export function usePlayModeFrame(\n callback: (state: RootState, delta: number) => void\n ): void;\n\n export type EngineMode = {\n editMode: boolean;\n playMode: boolean;\n };\n\n export function useEngineMode(): EngineMode;\n\n // Zustand-like store stub you generate\n export type SceneScriptState = {\n sceneProperties: Record<string, unknown>;\n };\n\n export function useSceneScriptStore(): SceneScriptState;\n export function useSceneScriptStore<T>(\n selector: (state: SceneScriptState) => T\n ): T;\n\n export function editEntities(\n edits: Array<{ id: number; newData: Partial<EntityData> }>\n ): void;\n\n export function useEntity(entityId: number): { entityData: EntityData };\n\n // ----------------------------\n // Components\n // ----------------------------\n export type EntityRenderRegistry = Record<\n string,\n React.FC<{ entityData: EntityData }>\n >;\n\n export const PhibelleRoot: React.FC<{\n entityRenderRegistry: EntityRenderRegistry;\n }>;\n}\n\nexport {};\n";