phibelle-kit 1.0.4 → 1.0.7

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.
@@ -1,238 +0,0 @@
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
- import type { PhibelleScene } from "./types.js";
7
-
8
- type SceneManifest = {
9
- sceneScript: string;
10
- entities: Record<string, string>;
11
- };
12
-
13
- async function getSceneFileContent(sceneId: string): Promise<string> {
14
- const fileUrl = await queryOnce<
15
- { sceneId: string },
16
- string | null
17
- >(api.scenes.getSceneFileUrl, { sceneId });
18
-
19
- if (fileUrl == null) {
20
- throw new Error("Scene file not found (scene may be missing or have no file yet)");
21
- }
22
-
23
- const response = await fetch(fileUrl);
24
- if (!response.ok) {
25
- throw new Error(`Failed to download scene file: ${response.status} ${response.statusText}`);
26
- }
27
- return response.text();
28
- }
29
-
30
- function parseSceneJson(content: string): PhibelleScene {
31
- try {
32
- return JSON.parse(content) as PhibelleScene;
33
- } catch {
34
- throw new Error("Invalid scene file: not valid JSON");
35
- }
36
- }
37
-
38
- /** Matches entity script filenames; group 1 is engine id. Use in both .test() and .match(). */
39
- export const ENTITY_FILE_RE = /^entity-(\d+)\.tsx$/;
40
-
41
- async function uploadSceneJsonAndUpdate(sceneId: string, json: string): Promise<void> {
42
- const uploadUrl = await mutateOnce(api.storage.generateUploadUrl, {});
43
- if (typeof uploadUrl !== "string") {
44
- throw new Error("Invalid upload URL response");
45
- }
46
- const uploadResult = await fetch(uploadUrl, {
47
- method: "POST",
48
- headers: { "Content-Type": "application/json" },
49
- body: json,
50
- });
51
- if (!uploadResult.ok) {
52
- throw new Error(`Failed to upload scene file: ${uploadResult.status} ${uploadResult.statusText}`);
53
- }
54
- const uploadResponse = (await uploadResult.json()) as { storageId?: string };
55
- const storageId = uploadResponse.storageId;
56
- if (!storageId) {
57
- throw new Error("Upload response missing storageId");
58
- }
59
- const sceneByteLength = Buffer.byteLength(json, "utf8");
60
- await mutateOnce(api.scenes.update, {
61
- id: sceneId as any,
62
- sceneFileId: storageId as any,
63
- sceneByteLength,
64
- lastModifiedSessionId: getSessionId(),
65
- });
66
- }
67
-
68
- export async function unpackageScene(sceneId: string, sceneDir: string): Promise<void> {
69
- const content = await getSceneFileContent(sceneId);
70
- const parsed = parseSceneJson(content);
71
-
72
- await fs.mkdir(sceneDir, { recursive: true });
73
-
74
- // Remove existing entity files so entities deleted in the editor are removed from disk
75
- try {
76
- const entries = await fs.readdir(sceneDir, { withFileTypes: true });
77
- for (const ent of entries) {
78
- if (ent.isFile() && ENTITY_FILE_RE.test(ent.name)) {
79
- await fs.unlink(path.join(sceneDir, ent.name));
80
- }
81
- }
82
- } catch {
83
- // Directory might not exist or be empty; ignore
84
- }
85
-
86
- const entities = parsed.entityStore?.entities ?? {};
87
- const manifest: SceneManifest = {
88
- sceneScript: "scene.tsx",
89
- entities: {},
90
- };
91
-
92
- await fs.writeFile(
93
- path.join(sceneDir, "scene.tsx"),
94
- parsed.sceneScriptStore?.sceneScript ?? "",
95
- "utf8"
96
- );
97
-
98
- for (const idStr of Object.keys(entities)) {
99
- const entity = entities[idStr];
100
- if (!entity || typeof entity.script !== "string") continue;
101
- const engineId = String(entity.engineId ?? idStr);
102
- const fileName = `entity-${engineId}.tsx`;
103
- manifest.entities[engineId] = fileName;
104
- await fs.writeFile(path.join(sceneDir, fileName), entity.script, "utf8");
105
- }
106
-
107
- await fs.writeFile(
108
- path.join(sceneDir, "manifest.json"),
109
- JSON.stringify(manifest, null, 2),
110
- "utf8"
111
- );
112
- }
113
-
114
- export async function pushScriptChange(
115
- sceneId: string,
116
- sceneDir: string,
117
- changedFile: string
118
- ): Promise<void> {
119
- const manifestPath = path.join(sceneDir, "manifest.json");
120
- let manifestRaw: string;
121
- try {
122
- manifestRaw = await fs.readFile(manifestPath, "utf8");
123
- } catch {
124
- throw new Error("manifest.json not found; run watch-scene to unpackage first");
125
- }
126
- const manifest = JSON.parse(manifestRaw) as SceneManifest;
127
-
128
- const content = await getSceneFileContent(sceneId);
129
- const parsed = parseSceneJson(content);
130
-
131
- const filePath = path.join(sceneDir, changedFile);
132
- let fileContent: string;
133
- try {
134
- fileContent = await fs.readFile(filePath, "utf8");
135
- } catch {
136
- throw new Error(`Could not read file: ${changedFile}`);
137
- }
138
-
139
- const isSceneScript = manifest.sceneScript === changedFile;
140
- if (isSceneScript) {
141
- if (!parsed.sceneScriptStore) parsed.sceneScriptStore = { sceneScript: "", sceneProperties: [], lastModifiedSceneScript: 0 };
142
- parsed.sceneScriptStore.sceneScript = fileContent;
143
- if (typeof parsed.sceneScriptStore.lastModifiedSceneScript === "number") {
144
- parsed.sceneScriptStore.lastModifiedSceneScript = Date.now();
145
- }
146
- } else {
147
- const entityId = Object.entries(manifest.entities).find(([, f]) => f === changedFile)?.[0];
148
- if (entityId == null) {
149
- throw new Error(`Unknown script file: ${changedFile}`);
150
- }
151
- const entities = parsed.entityStore?.entities ?? {};
152
- const entity = entities[entityId];
153
- if (!entity) {
154
- throw new Error(`Entity ${entityId} no longer in scene; skip or sync from app`);
155
- }
156
- entity.script = fileContent;
157
- entity.scriptVersion = (entity.scriptVersion ?? 0) + 1;
158
- }
159
-
160
- const json = JSON.stringify(parsed);
161
- await uploadSceneJsonAndUpdate(sceneId, json);
162
- }
163
-
164
- const DEFAULT_POSITION = '{"x":0,"y":0,"z":0}';
165
- const DEFAULT_ROTATION = '{"isEuler":true,"_x":0,"_y":0,"_z":0,"_order":"XYZ"}';
166
- const DEFAULT_SCALE = '{"x":1,"y":1,"z":1}';
167
-
168
- /** Add a new entity to the scene when user creates entity-<engineId>.tsx locally. */
169
- export async function pushNewEntity(
170
- sceneId: string,
171
- sceneDir: string,
172
- engineId: number
173
- ): Promise<void> {
174
- const manifestPath = path.join(sceneDir, "manifest.json");
175
- let manifestRaw: string;
176
- try {
177
- manifestRaw = await fs.readFile(manifestPath, "utf8");
178
- } catch {
179
- throw new Error("manifest.json not found; run watch-scene to unpackage first");
180
- }
181
- const manifest = JSON.parse(manifestRaw) as SceneManifest;
182
-
183
- const idStr = String(engineId);
184
- if (manifest.entities[idStr]) {
185
- throw new Error(`Entity ${engineId} already exists in manifest`);
186
- }
187
-
188
- const content = await getSceneFileContent(sceneId);
189
- const parsed = parseSceneJson(content);
190
-
191
- const fileName = `entity-${engineId}.tsx`;
192
- const filePath = path.join(sceneDir, fileName);
193
- let script: string;
194
- try {
195
- script = await fs.readFile(filePath, "utf8");
196
- } catch {
197
- throw new Error(`Could not read file: ${fileName}`);
198
- }
199
-
200
- if (!parsed.entityStore) {
201
- parsed.entityStore = {
202
- _currId: 0,
203
- rootEntities: [],
204
- entities: {},
205
- rootRenderVersion: 0,
206
- lastRenderedVersion: 0,
207
- selectedEntityIds: [],
208
- };
209
- }
210
- const store = parsed.entityStore;
211
- store._currId = Math.max(store._currId, engineId);
212
- if (!store.rootEntities.includes(engineId)) {
213
- store.rootEntities.push(engineId);
214
- }
215
- store.entities[idStr] = {
216
- name: `Entity-${engineId}`,
217
- engineId,
218
- childrenIdsSet: [],
219
- childrenIds: [],
220
- isNodeOpen: false,
221
- script,
222
- scriptVersion: 0,
223
- transform: {
224
- position: DEFAULT_POSITION,
225
- rotation: DEFAULT_ROTATION,
226
- scale: DEFAULT_SCALE,
227
- },
228
- renderVersion: 0,
229
- properties: [],
230
- threeId: 0,
231
- };
232
-
233
- manifest.entities[idStr] = fileName;
234
- await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf8");
235
-
236
- const json = JSON.stringify(parsed);
237
- await uploadSceneJsonAndUpdate(sceneId, json);
238
- }
@@ -1,103 +0,0 @@
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
-
6
- const DEBOUNCE_MS = 600;
7
-
8
- export type ScriptWatcher = { close: () => void };
9
-
10
- export type WatchCallbacks = {
11
- onPushed: (file: string) => void;
12
- onNewEntity: (engineId: number) => void;
13
- onError: (file: string, err: Error) => void;
14
- };
15
-
16
- export function createScriptWatcher(
17
- sceneId: string,
18
- sceneDir: string,
19
- onPushed: (file: string) => void,
20
- onNewEntity: (engineId: number) => void,
21
- onError: (file: string, err: Error) => void
22
- ): ScriptWatcher {
23
- const manifestPath = path.join(sceneDir, "manifest.json");
24
-
25
- function readManifest(): { sceneScript: string; entities: Record<string, string> } | null {
26
- try {
27
- const raw = fs.readFileSync(manifestPath, "utf8");
28
- return JSON.parse(raw);
29
- } catch {
30
- return null;
31
- }
32
- }
33
-
34
- let debounceTimer: ReturnType<typeof setTimeout> | null = null;
35
- let lastEvent: { type: "change" | "add"; file: string } | null = null;
36
-
37
- function schedule(type: "change" | "add", file: string) {
38
- lastEvent = { type, file };
39
- if (debounceTimer) clearTimeout(debounceTimer);
40
- debounceTimer = setTimeout(async () => {
41
- debounceTimer = null;
42
- const ev = lastEvent;
43
- lastEvent = null;
44
- if (!ev) return;
45
- const manifest = readManifest();
46
- if (!manifest) return;
47
-
48
- const basename = ev.file;
49
- if (basename === manifest.sceneScript) {
50
- try {
51
- await pushScriptChange(sceneId, sceneDir, basename);
52
- onPushed(basename);
53
- } catch (err) {
54
- onError(basename, err instanceof Error ? err : new Error(String(err)));
55
- }
56
- return;
57
- }
58
-
59
- const entityMatch = basename.match(ENTITY_FILE_RE);
60
- if (entityMatch) {
61
- const engineId = parseInt(entityMatch[1], 10);
62
- const isNew = !manifest.entities[String(engineId)];
63
- if (isNew) {
64
- try {
65
- await pushNewEntity(sceneId, sceneDir, engineId);
66
- onNewEntity(engineId);
67
- } catch (err) {
68
- onError(basename, err instanceof Error ? err : new Error(String(err)));
69
- }
70
- } else {
71
- try {
72
- await pushScriptChange(sceneId, sceneDir, basename);
73
- onPushed(basename);
74
- } catch (err) {
75
- onError(basename, err instanceof Error ? err : new Error(String(err)));
76
- }
77
- }
78
- }
79
- }, DEBOUNCE_MS);
80
- }
81
-
82
- const watcher = chokidar.watch(sceneDir, { ignoreInitial: true });
83
- watcher.on("change", (filePath: string) => {
84
- const basename = path.basename(filePath);
85
- if (basename === "manifest.json") return;
86
- if (basename === "scene.tsx" || ENTITY_FILE_RE.test(basename)) {
87
- schedule("change", basename);
88
- }
89
- });
90
- watcher.on("add", (filePath: string) => {
91
- const basename = path.basename(filePath);
92
- if (basename === "scene.tsx" || ENTITY_FILE_RE.test(basename)) {
93
- schedule("add", basename);
94
- }
95
- });
96
-
97
- return {
98
- close: () => {
99
- if (debounceTimer) clearTimeout(debounceTimer);
100
- watcher.close();
101
- },
102
- };
103
- }
package/src/lib/types.ts DELETED
@@ -1,63 +0,0 @@
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
-
15
- export type PhibelleScene = {
16
- entityStore: {
17
- _currId: number;
18
- rootEntities: number[];
19
- entities: {
20
- [id: string]: {
21
- name: string;
22
- engineId: number;
23
- childrenIdsSet: number[];
24
- childrenIds: number[];
25
- isNodeOpen: boolean;
26
- script: string;
27
- scriptVersion: number;
28
- transform: {
29
- position: string;
30
- rotation: string;
31
- scale: string;
32
- };
33
- renderVersion: number;
34
- properties: Array<{
35
- name: string;
36
- type: string;
37
- value: any;
38
- }>;
39
- threeId: number;
40
- }
41
- };
42
- rootRenderVersion: number;
43
- lastRenderedVersion: number;
44
- selectedEntityIds: number[];
45
- };
46
- editorCameraStore: {
47
- orthographic: boolean;
48
- cameraPosition: [number, number, number];
49
- cameraRotation: [number, number, number, string];
50
- targetPosition: [number, number, number];
51
- };
52
- errorStore: Record<string, unknown>;
53
- sceneScriptStore: {
54
- sceneScript: string;
55
- sceneProperties: any[];
56
- lastModifiedSceneScript: number;
57
- };
58
- engineModeStore: {
59
- engineMode: string;
60
- transformMode: string;
61
- };
62
- version: string;
63
- };
package/src/lib/utils.ts DELETED
@@ -1,3 +0,0 @@
1
- export function toErrorMessage(err: unknown): string {
2
- return err instanceof Error ? err.message : String(err);
3
- }
@@ -1,34 +0,0 @@
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
-
6
- const PORT = 13131;
7
-
8
- export function waitForToken(): Promise<string> {
9
- return new Promise((resolve) => {
10
- const server = http.createServer((req, res) => {
11
- const url = new URL(req.url!, `http://localhost`);
12
- const token = url.searchParams.get("token");
13
-
14
- res.setHeader("Access-Control-Allow-Origin", "*");
15
- res.setHeader("Access-Control-Allow-Methods", "GET");
16
-
17
- if (token) {
18
- res.writeHead(200, { "Content-Type": "text/html" });
19
- res.end("Token received");
20
- server.close();
21
- resolve(token);
22
- setToken(token);
23
- } else {
24
- res.writeHead(400);
25
- res.end("Missing token");
26
- }
27
- });
28
-
29
- server.listen(PORT, () => {
30
- console.log("Opening browser to authenticate...");
31
- open(`${BASE_URL}/cli-token`);
32
- });
33
- });
34
- }
@@ -1,117 +0,0 @@
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
- `
@@ -1,16 +0,0 @@
1
- export const getNpmPackage = (sceneId: string) => {
2
- return `{
3
- "scripts": {
4
- "watch": "npx 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
- }
@@ -1,47 +0,0 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import { getNpmPackage } from "./npm-package.js";
4
- import { GLOBAL_MODULE } from "./global-module.js";
5
-
6
- const PACKAGE_JSON = "package.json";
7
- const GLOBAL_D_TS = "global.d.ts";
8
-
9
- const PROBE_DEP = "react";
10
-
11
- export function setupSceneDirectory(sceneDir: string, sceneId: string) {
12
- const packagePath = path.join(sceneDir, PACKAGE_JSON);
13
- const globalDtsPath = path.join(sceneDir, GLOBAL_D_TS);
14
-
15
- let shouldPromptInstall = false;
16
-
17
- // 1. Create package.json
18
- const pkgJson = `${getNpmPackage(sceneId)}\n`;
19
- if (!fs.existsSync(packagePath) || fs.readFileSync(packagePath, "utf8") !== pkgJson) {
20
- fs.writeFileSync(packagePath, pkgJson);
21
- shouldPromptInstall = true;
22
- }
23
-
24
- // 2. Write global.d.ts
25
- fs.writeFileSync(globalDtsPath, GLOBAL_MODULE.trimStart());
26
-
27
- // 3. If node_modules doesn't have real deps yet, prompt install
28
- const probePath = path.join(sceneDir, "node_modules", PROBE_DEP, "package.json");
29
- if (!fs.existsSync(probePath)) {
30
- shouldPromptInstall = true;
31
- }
32
-
33
- return { shouldPromptInstall };
34
- }
35
-
36
- export function getInstallHint(sceneDir: string): string {
37
- const hasPnpm = fs.existsSync(path.join(sceneDir, "pnpm-lock.yaml"));
38
- const hasYarn = fs.existsSync(path.join(sceneDir, "yarn.lock"));
39
- const hasBun = fs.existsSync(path.join(sceneDir, "bun.lockb"));
40
- const hasNpm = fs.existsSync(path.join(sceneDir, "package-lock.json"));
41
-
42
- if (hasPnpm) return "pnpm install";
43
- if (hasYarn) return "yarn install";
44
- if (hasBun) return "bun install";
45
- if (hasNpm) return "npm install";
46
- return "npm install (or pnpm / yarn / bun install)";
47
- }
package/tsconfig.json DELETED
@@ -1,19 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "NodeNext",
5
- "moduleResolution": "NodeNext",
6
- "outDir": "./dist",
7
- "rootDir": "./src",
8
- "strict": true,
9
- "esModuleInterop": true,
10
- "skipLibCheck": true,
11
- "forceConsistentCasingInFileNames": true,
12
- "declaration": true,
13
- "resolveJsonModule": true,
14
- "allowJs": true,
15
- "checkJs": true
16
- },
17
- "include": ["src/**/*"],
18
- "exclude": ["node_modules", "dist"]
19
- }