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.
@@ -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, "scene.phibelle"), payload.sceneData, "utf8");
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 = getErrorText(error);
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 { 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
  }
@@ -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 manifestSceneId = getRequiredManifestSceneId(sceneDir);
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(manifestSceneId, lastPrintedEditorLink);
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 = await promptForInitialSyncChoice().catch((error) => {
76
- console.log(chalk.red(" ✖ " + toErrorMessage(error)));
77
- ws.close();
78
- return null;
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 updatedManifestSceneId = getRequiredManifestSceneId(sceneDir);
105
- lastPrintedEditorLink = printEditorLink(updatedManifestSceneId, lastPrintedEditorLink);
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 { 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
+ };
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
- 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
+ }
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
- console.log(chalk.yellow(" Choose initial sync direction:"));
173
- console.log(chalk.cyan(" [p] Push local scene to the web app"));
174
- console.log(chalk.cyan(" [a] Accept the current web app scene"));
175
- while (true) {
176
- const answer = (await askQuestion(" Initial sync [p/a]: ")).trim().toLowerCase();
177
- if (answer === "p" || answer === "push" || answer === "local")
178
- return "push-local";
179
- if (answer === "a" || answer === "accept" || answer === "web" || answer === "app")
180
- return "accept-web";
181
- 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();
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
- 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;
@@ -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,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
+ });
@@ -1,5 +1,6 @@
1
+ import { SCENE_FILE_NAME } from "../constants.js";
1
2
  export declare const SCENE_APP_DIR = "app";
2
- export declare const SCENE_FILE_NAME = "scene.phibelle";
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 const SCENE_FILE_NAME = "scene.phibelle";
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
- const MANIFEST_FILE_NAME = "manifest.json";
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: 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 ?? {},
@@ -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 === "manifest.json")
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];
@@ -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
+ ];
@@ -1 +1,3 @@
1
1
  export declare function toErrorMessage(err: unknown): string;
2
+ /** For exec/child_process errors: prefer stderr (last line), otherwise message. */
3
+ export declare function getExecErrorText(error: unknown): string;
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
+ }
@@ -1,4 +1,3 @@
1
1
  export declare function setupSceneDirectory(sceneDir: string): {
2
2
  shouldPromptInstall: boolean;
3
3
  };
4
- export declare function getInstallHint(sceneDir: string): string;
@@ -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.31",
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",