phibelle-kit 1.0.32 → 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.
@@ -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 { setupSceneDirectory, getInstallHint } from "../scene-setup/setup.js";
3
- export function initSceneCommand() {
4
- const sceneDir = process.cwd();
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(" Initializing scene..."));
7
- console.log(chalk.gray(` Dir: ${sceneDir}`));
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 { shouldPromptInstall } = setupSceneDirectory(sceneDir);
10
- console.log(chalk.green(" ✓ Created package.json, tsconfig.json, global.d.ts, AGENTS.md, CLAUDE.md, .gitignore"));
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."));
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
  }
@@ -4,20 +4,19 @@ import * as fs from "node:fs";
4
4
  import readline from "node:readline";
5
5
  import { WebSocketServer } from "ws";
6
6
  import { WS_PORT, SCENE_SYNC_TYPE, SCENE_FILE_NAME } from "../lib/constants.js";
7
- import { getRequiredManifestSceneId } from "../lib/manifest.js";
7
+ import { getRequiredManifestIds } from "../lib/manifest.js";
8
8
  import { unpackageScene, parseSceneJson, packageSceneFromFilesystem } from "../lib/scene/scene-packaging.js";
9
9
  import { createScriptWatcher } from "../lib/scene/script-watcher.js";
10
10
  import { toErrorMessage } from "../lib/utils.js";
11
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
12
11
  export const BASE_URL = process.env.NODE_ENV === "development" ? "http://localhost:3131" : "https://phibelle.studio";
13
12
  export async function watchSceneCommand() {
14
13
  const sceneDir = process.cwd();
15
- const manifestSceneId = getRequiredManifestSceneId(sceneDir);
14
+ const { sceneId, templateId } = getRequiredManifestIds(sceneDir);
16
15
  let lastPrintedEditorLink = null;
17
16
  console.log();
18
17
  console.log(chalk.bold.cyan(" Watching for scene sync (WebSocket)"));
19
18
  console.log(chalk.gray(` Dir: ${sceneDir}`));
20
- lastPrintedEditorLink = printEditorLink(manifestSceneId, lastPrintedEditorLink);
19
+ lastPrintedEditorLink = printEditorLink(sceneId, templateId, lastPrintedEditorLink);
21
20
  console.log(chalk.yellow(" Waiting for connection from local development server..."));
22
21
  console.log();
23
22
  console.log(chalk.gray(" Press Ctrl+C to stop"));
@@ -70,11 +69,13 @@ export async function watchSceneCommand() {
70
69
  startWatcher();
71
70
  return;
72
71
  }
73
- const choice = await promptForInitialSyncChoice().catch((error) => {
74
- console.log(chalk.red(" ✖ " + toErrorMessage(error)));
75
- ws.close();
76
- return null;
77
- });
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
+ });
78
79
  if (!choice || currentClient !== ws)
79
80
  return;
80
81
  if (choice === "push-local") {
@@ -96,11 +97,13 @@ export async function watchSceneCommand() {
96
97
  latestSceneJson = incoming.sceneData;
97
98
  const scenePath = path.join(sceneDir, SCENE_FILE_NAME);
98
99
  fs.writeFileSync(scenePath, incoming.sceneData, "utf8");
100
+ const resolvedTemplateId = incoming.templateId ??
101
+ (incoming.sceneId === "template" ? getRequiredManifestIds(sceneDir).templateId : undefined);
99
102
  try {
100
- await unpackageScene(incoming.sceneData, sceneDir, incoming.sceneId);
103
+ await unpackageScene(incoming.sceneData, sceneDir, incoming.sceneId, resolvedTemplateId);
101
104
  console.log(chalk.green(" ✓ Synced scene from app"));
102
- const updatedManifestSceneId = getRequiredManifestSceneId(sceneDir);
103
- lastPrintedEditorLink = printEditorLink(updatedManifestSceneId, lastPrintedEditorLink);
105
+ const updatedIds = getRequiredManifestIds(sceneDir);
106
+ lastPrintedEditorLink = printEditorLink(updatedIds.sceneId, updatedIds.templateId, lastPrintedEditorLink);
104
107
  }
105
108
  catch (e) {
106
109
  console.log(chalk.yellow(" ⚠ Unpackage: " + toErrorMessage(e)));
@@ -117,7 +120,6 @@ export async function watchSceneCommand() {
117
120
  scriptWatcher?.close();
118
121
  scriptWatcher = null;
119
122
  wss.close();
120
- rl.close();
121
123
  console.log(chalk.gray(" Goodbye"));
122
124
  console.log();
123
125
  process.exit(0);
@@ -128,7 +130,11 @@ function parseIncomingSceneMessage(raw) {
128
130
  try {
129
131
  const parsed = JSON.parse(raw);
130
132
  if (isSceneSyncMessage(parsed)) {
131
- return { sceneData: parsed.sceneData, sceneId: parsed.sceneId?.trim() || undefined };
133
+ return {
134
+ sceneData: parsed.sceneData,
135
+ sceneId: parsed.sceneId?.trim() || undefined,
136
+ templateId: parsed.templateId?.trim() || undefined,
137
+ };
132
138
  }
133
139
  }
134
140
  catch {
@@ -142,10 +148,14 @@ function isSceneSyncMessage(value) {
142
148
  const obj = value;
143
149
  return (obj.type === SCENE_SYNC_TYPE &&
144
150
  typeof obj.sceneData === "string" &&
145
- (obj.sceneId === undefined || typeof obj.sceneId === "string"));
151
+ (obj.sceneId === undefined || typeof obj.sceneId === "string") &&
152
+ (obj.templateId === undefined || typeof obj.templateId === "string"));
146
153
  }
147
- function printEditorLink(sceneId, lastPrintedLink) {
148
- const editorUrl = `${BASE_URL}/editor?sceneId=${encodeURIComponent(sceneId)}&wsEnabled=true`;
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
+ }
149
159
  if (editorUrl === lastPrintedLink)
150
160
  return lastPrintedLink;
151
161
  console.log(chalk.cyan(` Editor: ${editorUrl}`));
@@ -167,19 +177,25 @@ async function packageAndPersistLocalScene(sceneDir, sceneJson) {
167
177
  return packagedSceneJson;
168
178
  }
169
179
  async function promptForInitialSyncChoice() {
170
- console.log(chalk.yellow(" Choose initial sync direction:"));
171
- console.log(chalk.cyan(" [p] Push local scene to the web app"));
172
- console.log(chalk.cyan(" [a] Accept the current web app scene"));
173
- while (true) {
174
- const answer = (await askQuestion(" Initial sync [p/a]: ")).trim().toLowerCase();
175
- if (answer === "p" || answer === "push" || answer === "local")
176
- return "push-local";
177
- if (answer === "a" || answer === "accept" || answer === "web" || answer === "app")
178
- return "accept-web";
179
- console.log(chalk.yellow(" Please enter 'p' to push local or 'a' to accept the web app scene."));
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();
180
196
  }
181
197
  }
182
- function askQuestion(prompt) {
198
+ function askQuestion(rl, prompt) {
183
199
  return new Promise((resolve) => {
184
200
  rl.question(prompt, resolve);
185
201
  });
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
- const USAGE = " Usage: phibelle-kit watch | phibelle-kit clone";
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;
@@ -1,5 +1,12 @@
1
+ export type ManifestIds = {
2
+ sceneId: string;
3
+ /** Only present when sceneId === "template" */
4
+ templateId?: string;
5
+ };
1
6
  /**
2
- * Read manifest.json from sceneDir and return the required sceneId.
7
+ * Read manifest.json from sceneDir and return sceneId and optional templateId.
3
8
  * Throws with user-facing errors if manifest is missing, unreadable, invalid JSON, or missing sceneId.
4
9
  */
10
+ export declare function getRequiredManifestIds(sceneDir: string): ManifestIds;
11
+ /** @deprecated Use getRequiredManifestIds instead */
5
12
  export declare function getRequiredManifestSceneId(sceneDir: string): string;
@@ -2,13 +2,13 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { MANIFEST_FILE_NAME } from "./constants.js";
4
4
  /**
5
- * Read manifest.json from sceneDir and return the required sceneId.
5
+ * Read manifest.json from sceneDir and return sceneId and optional templateId.
6
6
  * Throws with user-facing errors if manifest is missing, unreadable, invalid JSON, or missing sceneId.
7
7
  */
8
- export function getRequiredManifestSceneId(sceneDir) {
8
+ export function getRequiredManifestIds(sceneDir) {
9
9
  const manifestPath = path.join(sceneDir, MANIFEST_FILE_NAME);
10
10
  if (!fs.existsSync(manifestPath)) {
11
- throw new Error(`Missing ${MANIFEST_FILE_NAME} in ${sceneDir}. Run "phibelle-kit clone" first or watch from a cloned scene folder.`);
11
+ throw new Error(`Missing ${MANIFEST_FILE_NAME} in ${sceneDir}. Run "phibelle-kit init" or "phibelle-kit clone" first.`);
12
12
  }
13
13
  let manifestRaw;
14
14
  try {
@@ -27,5 +27,14 @@ export function getRequiredManifestSceneId(sceneDir) {
27
27
  if (typeof manifest.sceneId !== "string" || !manifest.sceneId.trim()) {
28
28
  throw new Error(`Invalid ${MANIFEST_FILE_NAME}: missing required "sceneId".`);
29
29
  }
30
- return manifest.sceneId.trim();
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;
31
40
  }
@@ -0,0 +1,2 @@
1
+ /** Minimal valid scene JSON for init (empty entities; watch will sync from editor). */
2
+ export declare const MINIMAL_SCENE_JSON: string;
@@ -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
+ });
@@ -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>;
@@ -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: sceneId?.trim() || parsedSceneId,
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: typeof parsed.sceneId === "string" ? parsed.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 ?? {},
@@ -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];
@@ -0,0 +1,9 @@
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 const TEMPLATE_IDS = [
6
+ "basic-cube",
7
+ "maze",
8
+ "blocks-destroyer",
9
+ ];
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "phibelle-kit",
3
- "version": "1.0.32",
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",