phibelle-kit 1.0.31 → 1.0.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/clone-scene.js +4 -14
- package/dist/commands/init-scene.d.ts +1 -1
- package/dist/commands/init-scene.js +80 -11
- package/dist/commands/watch-scene.js +48 -58
- package/dist/index.js +6 -1
- package/dist/lib/constants.d.ts +12 -0
- package/dist/lib/constants.js +12 -0
- package/dist/lib/manifest.d.ts +12 -0
- package/dist/lib/manifest.js +40 -0
- package/dist/lib/minimal-scene.d.ts +2 -0
- package/dist/lib/minimal-scene.js +28 -0
- package/dist/lib/scene/file-system.d.ts +2 -1
- package/dist/lib/scene/file-system.js +3 -2
- package/dist/lib/scene/scene-packaging.d.ts +1 -1
- package/dist/lib/scene/scene-packaging.js +14 -6
- package/dist/lib/scene/script-watcher.js +3 -2
- package/dist/lib/template-ids.d.ts +6 -0
- package/dist/lib/template-ids.js +9 -0
- package/dist/lib/utils.d.ts +2 -0
- package/dist/lib/utils.js +10 -0
- package/dist/scene-setup/setup.d.ts +0 -1
- package/dist/scene-setup/setup.js +0 -15
- package/package.json +3 -2
|
@@ -4,11 +4,10 @@ import * as fs from "node:fs/promises";
|
|
|
4
4
|
import { execFile } from "node:child_process";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
import { WebSocketServer } from "ws";
|
|
7
|
+
import { WS_PORT, CLONE_REQUEST_TYPE, CLONE_RESPONSE_TYPE, SCENE_FILE_NAME, } from "../lib/constants.js";
|
|
8
|
+
import { getExecErrorText } from "../lib/utils.js";
|
|
7
9
|
import { parseSceneJson, unpackageScene } from "../lib/scene/scene-packaging.js";
|
|
8
10
|
import { setupSceneDirectory } from "../scene-setup/setup.js";
|
|
9
|
-
const WS_PORT = 31113;
|
|
10
|
-
const CLONE_REQUEST_TYPE = "phibelle-kit-clone-request";
|
|
11
|
-
const CLONE_RESPONSE_TYPE = "phibelle-kit-clone-response";
|
|
12
11
|
const CLONE_TIMEOUT_MS = 30_000;
|
|
13
12
|
const INITIAL_COMMIT_MESSAGE = "Initial scene clone from Phibelle";
|
|
14
13
|
const execFileAsync = promisify(execFile);
|
|
@@ -26,7 +25,7 @@ export async function cloneSceneCommand() {
|
|
|
26
25
|
await ensureDirectoryDoesNotExist(sceneDir);
|
|
27
26
|
await fs.mkdir(sceneDir, { recursive: false });
|
|
28
27
|
setupSceneDirectory(sceneDir);
|
|
29
|
-
await fs.writeFile(path.join(sceneDir,
|
|
28
|
+
await fs.writeFile(path.join(sceneDir, SCENE_FILE_NAME), payload.sceneData, "utf8");
|
|
30
29
|
await unpackageScene(payload.sceneData, sceneDir, payload.sceneId);
|
|
31
30
|
await initializeGitRepository(sceneDir);
|
|
32
31
|
console.log(chalk.green(" ✓ Scene cloned successfully"));
|
|
@@ -148,7 +147,7 @@ async function initializeGitRepository(sceneDir) {
|
|
|
148
147
|
}
|
|
149
148
|
catch (error) {
|
|
150
149
|
console.log(chalk.yellow(" ! Git initialization or first commit was skipped"));
|
|
151
|
-
const details =
|
|
150
|
+
const details = getExecErrorText(error);
|
|
152
151
|
if (details) {
|
|
153
152
|
console.log(chalk.gray(` ${details}`));
|
|
154
153
|
}
|
|
@@ -158,12 +157,3 @@ async function initializeGitRepository(sceneDir) {
|
|
|
158
157
|
async function runGit(args, cwd) {
|
|
159
158
|
await execFileAsync("git", args, { cwd });
|
|
160
159
|
}
|
|
161
|
-
function getErrorText(error) {
|
|
162
|
-
if (!error || typeof error !== "object")
|
|
163
|
-
return "";
|
|
164
|
-
const err = error;
|
|
165
|
-
const stderr = err.stderr?.trim();
|
|
166
|
-
if (stderr)
|
|
167
|
-
return stderr.split("\n").pop() ?? stderr;
|
|
168
|
-
return err.message?.trim() ?? "";
|
|
169
|
-
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function initSceneCommand(): void
|
|
1
|
+
export declare function initSceneCommand(): Promise<void>;
|
|
@@ -1,16 +1,85 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as fs from "node:fs/promises";
|
|
4
|
+
import readline from "node:readline";
|
|
5
|
+
import { SCENE_FILE_NAME } from "../lib/constants.js";
|
|
6
|
+
import { unpackageScene } from "../lib/scene/scene-packaging.js";
|
|
7
|
+
import { setupSceneDirectory } from "../scene-setup/setup.js";
|
|
8
|
+
import { MINIMAL_SCENE_JSON } from "../lib/minimal-scene.js";
|
|
9
|
+
import { TEMPLATE_IDS } from "../lib/template-ids.js";
|
|
10
|
+
function askQuestion(rl, prompt) {
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
rl.question(prompt, resolve);
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
function sanitizeProjectName(input) {
|
|
16
|
+
return input
|
|
17
|
+
.trim()
|
|
18
|
+
.replace(/[<>:"/\\|?*\x00-\x1F]/g, "-")
|
|
19
|
+
.replace(/\s+/g, "-")
|
|
20
|
+
.replace(/\.+$/g, "")
|
|
21
|
+
.replace(/-+/g, "-")
|
|
22
|
+
.replace(/^-|-$/g, "");
|
|
23
|
+
}
|
|
24
|
+
export async function initSceneCommand() {
|
|
25
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
26
|
+
const parentDir = process.cwd();
|
|
5
27
|
console.log();
|
|
6
|
-
console.log(chalk.bold.cyan("
|
|
7
|
-
console.log(chalk.gray(`
|
|
28
|
+
console.log(chalk.bold.cyan(" Initialize a new Phibelle scene project"));
|
|
29
|
+
console.log(chalk.gray(` Parent dir: ${parentDir}`));
|
|
8
30
|
console.log();
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
31
|
+
const projectName = await promptProjectName(rl);
|
|
32
|
+
const templateId = await promptTemplateId(rl);
|
|
33
|
+
const safeDirName = sanitizeProjectName(projectName) || "phibelle-scene";
|
|
34
|
+
const sceneDir = path.join(parentDir, safeDirName);
|
|
35
|
+
await ensureDirectoryDoesNotExist(sceneDir);
|
|
36
|
+
await fs.mkdir(sceneDir, { recursive: false });
|
|
37
|
+
setupSceneDirectory(sceneDir);
|
|
38
|
+
await fs.writeFile(path.join(sceneDir, SCENE_FILE_NAME), MINIMAL_SCENE_JSON, "utf8");
|
|
39
|
+
await unpackageScene(MINIMAL_SCENE_JSON, sceneDir, "template", templateId);
|
|
40
|
+
console.log(chalk.green(" ✓ Project initialized"));
|
|
41
|
+
console.log(chalk.gray(` Directory: ${sceneDir}`));
|
|
42
|
+
console.log(chalk.yellow(" Next steps:"));
|
|
43
|
+
console.log(chalk.cyan(` 1. cd "${safeDirName}"`));
|
|
44
|
+
console.log(chalk.cyan(` 2. npm install`));
|
|
45
|
+
console.log(chalk.cyan(` 3. npm run watch`));
|
|
46
|
+
console.log(chalk.gray(` Then open the editor URL shown by watch and accept the template scene to sync.`));
|
|
15
47
|
console.log();
|
|
48
|
+
rl.close();
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
async function promptProjectName(rl) {
|
|
52
|
+
while (true) {
|
|
53
|
+
const answer = await askQuestion(rl, chalk.cyan(" Project name: "));
|
|
54
|
+
const trimmed = answer.trim();
|
|
55
|
+
if (trimmed)
|
|
56
|
+
return trimmed;
|
|
57
|
+
console.log(chalk.yellow(" Please enter a non-empty project name."));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function promptTemplateId(rl) {
|
|
61
|
+
console.log(chalk.cyan(" Template:"));
|
|
62
|
+
TEMPLATE_IDS.forEach((id, i) => {
|
|
63
|
+
console.log(chalk.gray(` ${i + 1}. ${id}`));
|
|
64
|
+
});
|
|
65
|
+
while (true) {
|
|
66
|
+
const answer = (await askQuestion(rl, chalk.cyan(" Choose template [1-" + TEMPLATE_IDS.length + "]: "))).trim();
|
|
67
|
+
const num = parseInt(answer, 10);
|
|
68
|
+
if (num >= 1 && num <= TEMPLATE_IDS.length) {
|
|
69
|
+
return TEMPLATE_IDS[num - 1];
|
|
70
|
+
}
|
|
71
|
+
if (TEMPLATE_IDS.includes(answer)) {
|
|
72
|
+
return answer;
|
|
73
|
+
}
|
|
74
|
+
console.log(chalk.yellow(` Please enter 1-${TEMPLATE_IDS.length} or a template ID.`));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function ensureDirectoryDoesNotExist(sceneDir) {
|
|
78
|
+
try {
|
|
79
|
+
await fs.access(sceneDir);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
throw new Error(`Target folder already exists: ${sceneDir}. Choose a different project name.`);
|
|
16
85
|
}
|
|
@@ -1,25 +1,22 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import * as path from "path";
|
|
3
|
-
import * as fs from "fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import readline from "node:readline";
|
|
4
5
|
import { WebSocketServer } from "ws";
|
|
6
|
+
import { WS_PORT, SCENE_SYNC_TYPE, SCENE_FILE_NAME } from "../lib/constants.js";
|
|
7
|
+
import { getRequiredManifestIds } from "../lib/manifest.js";
|
|
5
8
|
import { unpackageScene, parseSceneJson, packageSceneFromFilesystem } from "../lib/scene/scene-packaging.js";
|
|
6
9
|
import { createScriptWatcher } from "../lib/scene/script-watcher.js";
|
|
7
10
|
import { toErrorMessage } from "../lib/utils.js";
|
|
8
|
-
import readline from "readline";
|
|
9
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
10
|
-
const WS_PORT = 31113;
|
|
11
|
-
const SCENE_SYNC_TYPE = "phibelle-kit-scene-sync";
|
|
12
|
-
const SCENE_FILE_NAME = "scene.phibelle";
|
|
13
|
-
const MANIFEST_FILE_NAME = "manifest.json";
|
|
14
11
|
export const BASE_URL = process.env.NODE_ENV === "development" ? "http://localhost:3131" : "https://phibelle.studio";
|
|
15
12
|
export async function watchSceneCommand() {
|
|
16
13
|
const sceneDir = process.cwd();
|
|
17
|
-
const
|
|
14
|
+
const { sceneId, templateId } = getRequiredManifestIds(sceneDir);
|
|
18
15
|
let lastPrintedEditorLink = null;
|
|
19
16
|
console.log();
|
|
20
17
|
console.log(chalk.bold.cyan(" Watching for scene sync (WebSocket)"));
|
|
21
18
|
console.log(chalk.gray(` Dir: ${sceneDir}`));
|
|
22
|
-
lastPrintedEditorLink = printEditorLink(
|
|
19
|
+
lastPrintedEditorLink = printEditorLink(sceneId, templateId, lastPrintedEditorLink);
|
|
23
20
|
console.log(chalk.yellow(" Waiting for connection from local development server..."));
|
|
24
21
|
console.log();
|
|
25
22
|
console.log(chalk.gray(" Press Ctrl+C to stop"));
|
|
@@ -72,11 +69,13 @@ export async function watchSceneCommand() {
|
|
|
72
69
|
startWatcher();
|
|
73
70
|
return;
|
|
74
71
|
}
|
|
75
|
-
const choice =
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
72
|
+
const choice = sceneId === "template"
|
|
73
|
+
? "push-local"
|
|
74
|
+
: await promptForInitialSyncChoice().catch((error) => {
|
|
75
|
+
console.log(chalk.red(" ✖ " + toErrorMessage(error)));
|
|
76
|
+
ws.close();
|
|
77
|
+
return null;
|
|
78
|
+
});
|
|
80
79
|
if (!choice || currentClient !== ws)
|
|
81
80
|
return;
|
|
82
81
|
if (choice === "push-local") {
|
|
@@ -98,11 +97,13 @@ export async function watchSceneCommand() {
|
|
|
98
97
|
latestSceneJson = incoming.sceneData;
|
|
99
98
|
const scenePath = path.join(sceneDir, SCENE_FILE_NAME);
|
|
100
99
|
fs.writeFileSync(scenePath, incoming.sceneData, "utf8");
|
|
100
|
+
const resolvedTemplateId = incoming.templateId ??
|
|
101
|
+
(incoming.sceneId === "template" ? getRequiredManifestIds(sceneDir).templateId : undefined);
|
|
101
102
|
try {
|
|
102
|
-
await unpackageScene(incoming.sceneData, sceneDir, incoming.sceneId);
|
|
103
|
+
await unpackageScene(incoming.sceneData, sceneDir, incoming.sceneId, resolvedTemplateId);
|
|
103
104
|
console.log(chalk.green(" ✓ Synced scene from app"));
|
|
104
|
-
const
|
|
105
|
-
lastPrintedEditorLink = printEditorLink(
|
|
105
|
+
const updatedIds = getRequiredManifestIds(sceneDir);
|
|
106
|
+
lastPrintedEditorLink = printEditorLink(updatedIds.sceneId, updatedIds.templateId, lastPrintedEditorLink);
|
|
106
107
|
}
|
|
107
108
|
catch (e) {
|
|
108
109
|
console.log(chalk.yellow(" ⚠ Unpackage: " + toErrorMessage(e)));
|
|
@@ -119,7 +120,6 @@ export async function watchSceneCommand() {
|
|
|
119
120
|
scriptWatcher?.close();
|
|
120
121
|
scriptWatcher = null;
|
|
121
122
|
wss.close();
|
|
122
|
-
rl.close();
|
|
123
123
|
console.log(chalk.gray(" Goodbye"));
|
|
124
124
|
console.log();
|
|
125
125
|
process.exit(0);
|
|
@@ -130,7 +130,11 @@ function parseIncomingSceneMessage(raw) {
|
|
|
130
130
|
try {
|
|
131
131
|
const parsed = JSON.parse(raw);
|
|
132
132
|
if (isSceneSyncMessage(parsed)) {
|
|
133
|
-
return {
|
|
133
|
+
return {
|
|
134
|
+
sceneData: parsed.sceneData,
|
|
135
|
+
sceneId: parsed.sceneId?.trim() || undefined,
|
|
136
|
+
templateId: parsed.templateId?.trim() || undefined,
|
|
137
|
+
};
|
|
134
138
|
}
|
|
135
139
|
}
|
|
136
140
|
catch {
|
|
@@ -144,10 +148,14 @@ function isSceneSyncMessage(value) {
|
|
|
144
148
|
const obj = value;
|
|
145
149
|
return (obj.type === SCENE_SYNC_TYPE &&
|
|
146
150
|
typeof obj.sceneData === "string" &&
|
|
147
|
-
(obj.sceneId === undefined || typeof obj.sceneId === "string")
|
|
151
|
+
(obj.sceneId === undefined || typeof obj.sceneId === "string") &&
|
|
152
|
+
(obj.templateId === undefined || typeof obj.templateId === "string"));
|
|
148
153
|
}
|
|
149
|
-
function printEditorLink(sceneId, lastPrintedLink) {
|
|
150
|
-
|
|
154
|
+
function printEditorLink(sceneId, templateId, lastPrintedLink) {
|
|
155
|
+
let editorUrl = `${BASE_URL}/editor?sceneId=${encodeURIComponent(sceneId)}&wsEnabled=true`;
|
|
156
|
+
if (sceneId === "template" && templateId) {
|
|
157
|
+
editorUrl += `&id=${encodeURIComponent(templateId)}`;
|
|
158
|
+
}
|
|
151
159
|
if (editorUrl === lastPrintedLink)
|
|
152
160
|
return lastPrintedLink;
|
|
153
161
|
console.log(chalk.cyan(` Editor: ${editorUrl}`));
|
|
@@ -169,44 +177,26 @@ async function packageAndPersistLocalScene(sceneDir, sceneJson) {
|
|
|
169
177
|
return packagedSceneJson;
|
|
170
178
|
}
|
|
171
179
|
async function promptForInitialSyncChoice() {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
180
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
181
|
+
try {
|
|
182
|
+
console.log(chalk.yellow(" Choose initial sync direction:"));
|
|
183
|
+
console.log(chalk.cyan(" [p] Push local scene to the web app"));
|
|
184
|
+
console.log(chalk.cyan(" [a] Accept the current web app scene"));
|
|
185
|
+
while (true) {
|
|
186
|
+
const answer = (await askQuestion(rl, " Initial sync [p/a]: ")).trim().toLowerCase();
|
|
187
|
+
if (answer === "p" || answer === "push" || answer === "local")
|
|
188
|
+
return "push-local";
|
|
189
|
+
if (answer === "a" || answer === "accept" || answer === "web" || answer === "app")
|
|
190
|
+
return "accept-web";
|
|
191
|
+
console.log(chalk.yellow(" Please enter 'p' to push local or 'a' to accept the web app scene."));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
rl.close();
|
|
182
196
|
}
|
|
183
197
|
}
|
|
184
|
-
function askQuestion(prompt) {
|
|
198
|
+
function askQuestion(rl, prompt) {
|
|
185
199
|
return new Promise((resolve) => {
|
|
186
200
|
rl.question(prompt, resolve);
|
|
187
201
|
});
|
|
188
202
|
}
|
|
189
|
-
function getRequiredManifestSceneId(sceneDir) {
|
|
190
|
-
const manifestPath = path.join(sceneDir, MANIFEST_FILE_NAME);
|
|
191
|
-
if (!fs.existsSync(manifestPath)) {
|
|
192
|
-
throw new Error(`Missing ${MANIFEST_FILE_NAME} in ${sceneDir}. Run "phibelle-kit clone" first or watch from a cloned scene folder.`);
|
|
193
|
-
}
|
|
194
|
-
let manifestRaw;
|
|
195
|
-
try {
|
|
196
|
-
manifestRaw = fs.readFileSync(manifestPath, "utf8");
|
|
197
|
-
}
|
|
198
|
-
catch {
|
|
199
|
-
throw new Error(`Failed to read ${MANIFEST_FILE_NAME} in ${sceneDir}.`);
|
|
200
|
-
}
|
|
201
|
-
let manifest;
|
|
202
|
-
try {
|
|
203
|
-
manifest = JSON.parse(manifestRaw);
|
|
204
|
-
}
|
|
205
|
-
catch {
|
|
206
|
-
throw new Error(`Invalid ${MANIFEST_FILE_NAME}: expected valid JSON.`);
|
|
207
|
-
}
|
|
208
|
-
if (typeof manifest.sceneId !== "string" || !manifest.sceneId.trim()) {
|
|
209
|
-
throw new Error(`Invalid ${MANIFEST_FILE_NAME}: missing required "sceneId".`);
|
|
210
|
-
}
|
|
211
|
-
return manifest.sceneId.trim();
|
|
212
|
-
}
|
package/dist/index.js
CHANGED
|
@@ -2,9 +2,14 @@
|
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { watchSceneCommand } from "./commands/watch-scene.js";
|
|
4
4
|
import { cloneSceneCommand } from "./commands/clone-scene.js";
|
|
5
|
-
|
|
5
|
+
import { initSceneCommand } from "./commands/init-scene.js";
|
|
6
|
+
const USAGE = " Usage: phibelle-kit init | phibelle-kit watch | phibelle-kit clone";
|
|
6
7
|
async function main() {
|
|
7
8
|
const command = process.argv[2];
|
|
9
|
+
if (command === "init") {
|
|
10
|
+
await initSceneCommand();
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
8
13
|
if (command === "watch") {
|
|
9
14
|
await watchSceneCommand();
|
|
10
15
|
return;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** WebSocket port for phibelle-kit CLI ↔ web app communication. Must match app's use-cli-websocket.ts */
|
|
2
|
+
export declare const WS_PORT = 31113;
|
|
3
|
+
/** Scene JSON file in cloned scene directory */
|
|
4
|
+
export declare const SCENE_FILE_NAME = "scene.phibelle";
|
|
5
|
+
/** Manifest file mapping entity IDs to filesystem paths */
|
|
6
|
+
export declare const MANIFEST_FILE_NAME = "manifest.json";
|
|
7
|
+
/** Message type for scene sync over WebSocket */
|
|
8
|
+
export declare const SCENE_SYNC_TYPE = "phibelle-kit-scene-sync";
|
|
9
|
+
/** Message type for clone request from kit to app */
|
|
10
|
+
export declare const CLONE_REQUEST_TYPE = "phibelle-kit-clone-request";
|
|
11
|
+
/** Message type for clone response from app to kit */
|
|
12
|
+
export declare const CLONE_RESPONSE_TYPE = "phibelle-kit-clone-response";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** WebSocket port for phibelle-kit CLI ↔ web app communication. Must match app's use-cli-websocket.ts */
|
|
2
|
+
export const WS_PORT = 31113;
|
|
3
|
+
/** Scene JSON file in cloned scene directory */
|
|
4
|
+
export const SCENE_FILE_NAME = "scene.phibelle";
|
|
5
|
+
/** Manifest file mapping entity IDs to filesystem paths */
|
|
6
|
+
export const MANIFEST_FILE_NAME = "manifest.json";
|
|
7
|
+
/** Message type for scene sync over WebSocket */
|
|
8
|
+
export const SCENE_SYNC_TYPE = "phibelle-kit-scene-sync";
|
|
9
|
+
/** Message type for clone request from kit to app */
|
|
10
|
+
export const CLONE_REQUEST_TYPE = "phibelle-kit-clone-request";
|
|
11
|
+
/** Message type for clone response from app to kit */
|
|
12
|
+
export const CLONE_RESPONSE_TYPE = "phibelle-kit-clone-response";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type ManifestIds = {
|
|
2
|
+
sceneId: string;
|
|
3
|
+
/** Only present when sceneId === "template" */
|
|
4
|
+
templateId?: string;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Read manifest.json from sceneDir and return sceneId and optional templateId.
|
|
8
|
+
* Throws with user-facing errors if manifest is missing, unreadable, invalid JSON, or missing sceneId.
|
|
9
|
+
*/
|
|
10
|
+
export declare function getRequiredManifestIds(sceneDir: string): ManifestIds;
|
|
11
|
+
/** @deprecated Use getRequiredManifestIds instead */
|
|
12
|
+
export declare function getRequiredManifestSceneId(sceneDir: string): string;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { MANIFEST_FILE_NAME } from "./constants.js";
|
|
4
|
+
/**
|
|
5
|
+
* Read manifest.json from sceneDir and return sceneId and optional templateId.
|
|
6
|
+
* Throws with user-facing errors if manifest is missing, unreadable, invalid JSON, or missing sceneId.
|
|
7
|
+
*/
|
|
8
|
+
export function getRequiredManifestIds(sceneDir) {
|
|
9
|
+
const manifestPath = path.join(sceneDir, MANIFEST_FILE_NAME);
|
|
10
|
+
if (!fs.existsSync(manifestPath)) {
|
|
11
|
+
throw new Error(`Missing ${MANIFEST_FILE_NAME} in ${sceneDir}. Run "phibelle-kit init" or "phibelle-kit clone" first.`);
|
|
12
|
+
}
|
|
13
|
+
let manifestRaw;
|
|
14
|
+
try {
|
|
15
|
+
manifestRaw = fs.readFileSync(manifestPath, "utf8");
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
throw new Error(`Failed to read ${MANIFEST_FILE_NAME} in ${sceneDir}.`);
|
|
19
|
+
}
|
|
20
|
+
let manifest;
|
|
21
|
+
try {
|
|
22
|
+
manifest = JSON.parse(manifestRaw);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
throw new Error(`Invalid ${MANIFEST_FILE_NAME}: expected valid JSON.`);
|
|
26
|
+
}
|
|
27
|
+
if (typeof manifest.sceneId !== "string" || !manifest.sceneId.trim()) {
|
|
28
|
+
throw new Error(`Invalid ${MANIFEST_FILE_NAME}: missing required "sceneId".`);
|
|
29
|
+
}
|
|
30
|
+
const sceneId = manifest.sceneId.trim();
|
|
31
|
+
const result = { sceneId };
|
|
32
|
+
if (sceneId === "template" && typeof manifest.templateId === "string" && manifest.templateId.trim()) {
|
|
33
|
+
result.templateId = manifest.templateId.trim();
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
/** @deprecated Use getRequiredManifestIds instead */
|
|
38
|
+
export function getRequiredManifestSceneId(sceneDir) {
|
|
39
|
+
return getRequiredManifestIds(sceneDir).sceneId;
|
|
40
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Minimal valid scene JSON for init (empty entities; watch will sync from editor). */
|
|
2
|
+
export const MINIMAL_SCENE_JSON = JSON.stringify({
|
|
3
|
+
entityStore: {
|
|
4
|
+
_currId: 0,
|
|
5
|
+
rootEntities: [],
|
|
6
|
+
entities: {},
|
|
7
|
+
rootRenderVersion: 0,
|
|
8
|
+
lastRenderedVersion: 0,
|
|
9
|
+
selectedEntityIds: [],
|
|
10
|
+
},
|
|
11
|
+
editorCameraStore: {
|
|
12
|
+
orthographic: false,
|
|
13
|
+
cameraPosition: [0, 0, 0],
|
|
14
|
+
cameraRotation: [0, 0, 0, "XYZ"],
|
|
15
|
+
targetPosition: [0, 0, 0],
|
|
16
|
+
},
|
|
17
|
+
errorStore: {},
|
|
18
|
+
sceneScriptStore: {
|
|
19
|
+
sceneScript: "// Empty scene script. Run phibelle-kit watch to start the scene.\n",
|
|
20
|
+
sceneProperties: [],
|
|
21
|
+
lastModifiedSceneScript: 0,
|
|
22
|
+
},
|
|
23
|
+
engineModeStore: {
|
|
24
|
+
engineMode: "edit",
|
|
25
|
+
transformMode: "translate",
|
|
26
|
+
},
|
|
27
|
+
version: "1",
|
|
28
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { SCENE_FILE_NAME } from "../constants.js";
|
|
1
2
|
export declare const SCENE_APP_DIR = "app";
|
|
2
|
-
export
|
|
3
|
+
export { SCENE_FILE_NAME };
|
|
3
4
|
export declare function getSceneAppDir(sceneDir: string): string;
|
|
4
5
|
export declare function folderExists(folderPath: string): boolean;
|
|
5
6
|
export declare function createFolder(folderPath: string): void;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
-
import * as fs from "fs";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import { SCENE_FILE_NAME } from "../constants.js";
|
|
3
4
|
export const SCENE_APP_DIR = "app";
|
|
4
|
-
export
|
|
5
|
+
export { SCENE_FILE_NAME };
|
|
5
6
|
export function getSceneAppDir(sceneDir) {
|
|
6
7
|
return path.join(sceneDir, SCENE_APP_DIR);
|
|
7
8
|
}
|
|
@@ -12,6 +12,6 @@ export declare function entityNameToSlug(name: string): string;
|
|
|
12
12
|
/** Convert slug to display name: "main-enemy" -> "Main Enemy". */
|
|
13
13
|
export declare function slugToEntityName(slug: string): string;
|
|
14
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>;
|
|
15
|
+
export declare function unpackageScene(sceneJson: string, sceneDir: string, sceneId?: string, templateId?: string): Promise<void>;
|
|
16
16
|
export declare function patchSceneFromFile(sceneJson: string, sceneDir: string, _relativePath: string, _eventType: "change" | "add" | "unlink" | "addDir" | "unlinkDir"): Promise<string>;
|
|
17
17
|
export declare function packageSceneFromFilesystem(sceneJson: string, sceneDir: string): Promise<string>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import * as fs from "fs/promises";
|
|
2
|
-
import * as path from "path";
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
3
|
import { getSceneAppDir } from "./file-system.js";
|
|
4
|
-
|
|
4
|
+
import { MANIFEST_FILE_NAME } from "../constants.js";
|
|
5
5
|
const LEGACY_SCENE_FILE = "scene.tsx";
|
|
6
6
|
const CHILDREN_DIR_NAME = "children";
|
|
7
7
|
/** Scene-level file names in app/. */
|
|
@@ -40,7 +40,7 @@ export function slugToEntityName(slug) {
|
|
|
40
40
|
.join(" ");
|
|
41
41
|
}
|
|
42
42
|
/** Unpackage scene JSON to filesystem. No DB; accepts full scene JSON string. */
|
|
43
|
-
export async function unpackageScene(sceneJson, sceneDir, sceneId) {
|
|
43
|
+
export async function unpackageScene(sceneJson, sceneDir, sceneId, templateId) {
|
|
44
44
|
const parsed = parseSceneJson(sceneJson);
|
|
45
45
|
const parsedSceneId = typeof parsed._id === "string"
|
|
46
46
|
? parsed._id
|
|
@@ -50,8 +50,12 @@ export async function unpackageScene(sceneJson, sceneDir, sceneId) {
|
|
|
50
50
|
await fs.mkdir(appDir, { recursive: true });
|
|
51
51
|
await cleanupKnownEntityDirectories(appDir, previousManifest);
|
|
52
52
|
await cleanupLegacySceneFiles(appDir);
|
|
53
|
+
const resolvedSceneId = sceneId?.trim() || parsedSceneId;
|
|
53
54
|
const manifest = {
|
|
54
|
-
sceneId:
|
|
55
|
+
sceneId: resolvedSceneId,
|
|
56
|
+
...(resolvedSceneId === "template" && templateId?.trim()
|
|
57
|
+
? { templateId: templateId.trim() }
|
|
58
|
+
: {}),
|
|
55
59
|
sceneScript: SCENE_SCRIPT_FILE,
|
|
56
60
|
sceneProperties: SCENE_PROPERTIES_FILE,
|
|
57
61
|
entities: {},
|
|
@@ -136,8 +140,12 @@ async function readManifest(sceneDir) {
|
|
|
136
140
|
try {
|
|
137
141
|
const manifestRaw = await fs.readFile(path.join(sceneDir, MANIFEST_FILE_NAME), "utf8");
|
|
138
142
|
const parsed = JSON.parse(manifestRaw);
|
|
143
|
+
const sceneId = typeof parsed.sceneId === "string" ? parsed.sceneId : "";
|
|
139
144
|
return {
|
|
140
|
-
sceneId
|
|
145
|
+
sceneId,
|
|
146
|
+
...(sceneId === "template" && typeof parsed.templateId === "string" && parsed.templateId.trim()
|
|
147
|
+
? { templateId: parsed.templateId.trim() }
|
|
148
|
+
: {}),
|
|
141
149
|
sceneScript: typeof parsed.sceneScript === "string" ? parsed.sceneScript : SCENE_SCRIPT_FILE,
|
|
142
150
|
sceneProperties: typeof parsed.sceneProperties === "string" ? parsed.sceneProperties : SCENE_PROPERTIES_FILE,
|
|
143
151
|
entities: parsed.entities ?? {},
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import * as path from "path";
|
|
1
|
+
import * as path from "node:path";
|
|
2
2
|
import chokidar from "chokidar";
|
|
3
|
+
import { MANIFEST_FILE_NAME } from "../constants.js";
|
|
3
4
|
import { getSceneAppDir } from "./file-system.js";
|
|
4
5
|
import { packageSceneFromFilesystem, ENTITY_SCRIPT_FILE, ENTITY_PROPERTIES_FILE, ENTITY_TRANSFORMS_FILE, SCENE_SCRIPT_FILE, SCENE_PROPERTIES_FILE, } from "./scene-packaging.js";
|
|
5
6
|
const DEBOUNCE_MS = 100;
|
|
@@ -48,7 +49,7 @@ export function createScriptWatcher(sceneDir, getLatestSceneJson, onSceneUpdated
|
|
|
48
49
|
const watcher = chokidar.watch(appDir, { ignoreInitial: true });
|
|
49
50
|
function handlePathEvent(filePath) {
|
|
50
51
|
const relativePath = toRelativePath(filePath);
|
|
51
|
-
if (relativePath ===
|
|
52
|
+
if (relativePath === MANIFEST_FILE_NAME)
|
|
52
53
|
return;
|
|
53
54
|
if (isRelevantPath(relativePath))
|
|
54
55
|
schedule(relativePath);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template IDs - must match src/lib/templates/template-ids.ts in the main app.
|
|
3
|
+
* Used by init command for template selection.
|
|
4
|
+
*/
|
|
5
|
+
export declare const TEMPLATE_IDS: readonly ["basic-cube", "maze", "blocks-destroyer"];
|
|
6
|
+
export type TemplateId = (typeof TEMPLATE_IDS)[number];
|
package/dist/lib/utils.d.ts
CHANGED
package/dist/lib/utils.js
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
1
|
export function toErrorMessage(err) {
|
|
2
2
|
return err instanceof Error ? err.message : String(err);
|
|
3
3
|
}
|
|
4
|
+
/** For exec/child_process errors: prefer stderr (last line), otherwise message. */
|
|
5
|
+
export function getExecErrorText(error) {
|
|
6
|
+
if (!error || typeof error !== "object")
|
|
7
|
+
return "";
|
|
8
|
+
const err = error;
|
|
9
|
+
const stderr = err.stderr?.trim();
|
|
10
|
+
if (stderr)
|
|
11
|
+
return stderr.split("\n").pop() ?? stderr;
|
|
12
|
+
return err.message?.trim() ?? "";
|
|
13
|
+
}
|
|
@@ -39,18 +39,3 @@ export function setupSceneDirectory(sceneDir) {
|
|
|
39
39
|
}
|
|
40
40
|
return { shouldPromptInstall };
|
|
41
41
|
}
|
|
42
|
-
export function getInstallHint(sceneDir) {
|
|
43
|
-
const hasPnpm = fs.existsSync(path.join(sceneDir, "pnpm-lock.yaml"));
|
|
44
|
-
const hasYarn = fs.existsSync(path.join(sceneDir, "yarn.lock"));
|
|
45
|
-
const hasBun = fs.existsSync(path.join(sceneDir, "bun.lockb"));
|
|
46
|
-
const hasNpm = fs.existsSync(path.join(sceneDir, "package-lock.json"));
|
|
47
|
-
if (hasPnpm)
|
|
48
|
-
return "pnpm install";
|
|
49
|
-
if (hasBun)
|
|
50
|
-
return "bun install";
|
|
51
|
-
if (hasYarn)
|
|
52
|
-
return "yarn install";
|
|
53
|
-
if (hasNpm)
|
|
54
|
-
return "npm install";
|
|
55
|
-
return "npm install (or pnpm / bun / yarn install)";
|
|
56
|
-
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "phibelle-kit",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.33",
|
|
4
4
|
"description": "CLI tool for interacting with the Phibelle engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"phibelle-kit": "./dist/index.js"
|
|
7
|
+
"phibelle-kit": "./dist/index.js",
|
|
8
|
+
"create-phibelle": "./dist/index.js init"
|
|
8
9
|
},
|
|
9
10
|
"scripts": {
|
|
10
11
|
"build": "tsc",
|