@viji-dev/sdk 1.0.0 → 1.0.1

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.
Files changed (77) hide show
  1. package/README.md +155 -60
  2. package/bin/viji.js +9 -29
  3. package/dist/assets/artist-dts-BHUsvSI6.js +613 -0
  4. package/dist/assets/artist-dts-p5-Cyw8vmy_.js +736 -0
  5. package/dist/assets/core-CiQx3w0t.js +12 -0
  6. package/dist/assets/dark-plus-C3mMm8J8.js +1 -0
  7. package/dist/assets/docs-api-PBLtY4Ni.js +12381 -0
  8. package/dist/assets/engine-javascript-CXyY7cc8.js +141 -0
  9. package/dist/assets/essentia-wasm.web-0S-sW98u-CYV1l1zv.js +38 -0
  10. package/dist/assets/essentia.js-core.es-DnrJE0uR-DOSrF5_G.js +32 -0
  11. package/dist/assets/glsl-DMyvO4G4.js +1 -0
  12. package/dist/assets/index-BhFxsauQ.js +215 -0
  13. package/dist/assets/index-BqhVeA7U.css +1 -0
  14. package/dist/assets/index-T4TOjvD0.js +1 -0
  15. package/dist/assets/index-Wz9WqGqz.js +52 -0
  16. package/dist/assets/index-t24aGwla.js +1 -0
  17. package/dist/assets/javascript-wDzz0qaB.js +1 -0
  18. package/dist/assets/shader-uniforms-GdaUkQPK.js +1 -0
  19. package/dist/assets/typescript-BPQ3VLAy.js +1 -0
  20. package/dist/assets/viji.worker-CQSJ0SiO-ljtBlcNZ.js +27018 -0
  21. package/{index.html → dist/index.html} +2 -1
  22. package/package.json +31 -35
  23. package/src/cli/commands/build.js +50 -99
  24. package/src/cli/commands/create.js +32 -47
  25. package/src/cli/commands/dev.js +30 -97
  26. package/src/cli/server/dev-server.js +233 -0
  27. package/src/cli/server/scene-scanner.js +93 -0
  28. package/src/cli/server/vite-scene-plugin.d.ts +2 -0
  29. package/src/cli/server/vite-scene-plugin.js +134 -0
  30. package/src/cli/utils/cli-utils.js +29 -139
  31. package/src/cli/utils/scene-compiler.js +10 -17
  32. package/src/templates/scene-templates.js +85 -0
  33. package/.gitignore +0 -29
  34. package/eslint.config.js +0 -37
  35. package/postcss.config.js +0 -6
  36. package/scenes/audio-visualizer/main.js +0 -287
  37. package/scenes/core-demo/main.js +0 -532
  38. package/scenes/demo-scene/main.js +0 -619
  39. package/scenes/global.d.ts +0 -15
  40. package/scenes/particle-system/main.js +0 -349
  41. package/scenes/tsconfig.json +0 -12
  42. package/scenes/video-mirror/main.ts +0 -436
  43. package/src/App.css +0 -42
  44. package/src/App.tsx +0 -279
  45. package/src/cli/commands/init.js +0 -262
  46. package/src/components/SDKPage.tsx +0 -337
  47. package/src/components/core/CoreContainer.tsx +0 -126
  48. package/src/components/ui/DeviceSelectionList.tsx +0 -137
  49. package/src/components/ui/FPSCounter.tsx +0 -78
  50. package/src/components/ui/FileDropzonePanel.tsx +0 -120
  51. package/src/components/ui/FileListPanel.tsx +0 -285
  52. package/src/components/ui/InputExpansionPanel.tsx +0 -31
  53. package/src/components/ui/MediaPlayerControls.tsx +0 -191
  54. package/src/components/ui/MenuContainer.tsx +0 -71
  55. package/src/components/ui/ParametersMenu.tsx +0 -797
  56. package/src/components/ui/ProjectSwitcherMenu.tsx +0 -192
  57. package/src/components/ui/QuickInputControls.tsx +0 -542
  58. package/src/components/ui/SDKMenuSystem.tsx +0 -96
  59. package/src/components/ui/SettingsMenu.tsx +0 -346
  60. package/src/components/ui/SimpleInputControls.tsx +0 -137
  61. package/src/index.css +0 -68
  62. package/src/main.tsx +0 -10
  63. package/src/scenes-hmr.ts +0 -158
  64. package/src/services/project-filesystem.ts +0 -436
  65. package/src/stores/scene-player/index.ts +0 -3
  66. package/src/stores/scene-player/input-manager.store.ts +0 -1045
  67. package/src/stores/scene-player/scene-session.store.ts +0 -659
  68. package/src/styles/globals.css +0 -111
  69. package/src/templates/minimal-template.js +0 -11
  70. package/src/utils/debounce.js +0 -34
  71. package/src/vite-env.d.ts +0 -1
  72. package/tailwind.config.js +0 -18
  73. package/tsconfig.app.json +0 -27
  74. package/tsconfig.json +0 -27
  75. package/tsconfig.node.json +0 -27
  76. package/vite.config.ts +0 -54
  77. /package/{public → dist}/favicon.png +0 -0
@@ -0,0 +1,233 @@
1
+ import http from 'node:http';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { exec } from 'node:child_process';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { WebSocketServer } from 'ws';
7
+ import chokidar from 'chokidar';
8
+ import { scanScenes, scanSingleScene, readSceneCode } from './scene-scanner.js';
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+
13
+ const MIME_TYPES = {
14
+ '.html': 'text/html',
15
+ '.js': 'application/javascript',
16
+ '.mjs': 'application/javascript',
17
+ '.css': 'text/css',
18
+ '.json': 'application/json',
19
+ '.png': 'image/png',
20
+ '.jpg': 'image/jpeg',
21
+ '.svg': 'image/svg+xml',
22
+ '.ico': 'image/x-icon',
23
+ '.woff': 'font/woff',
24
+ '.woff2': 'font/woff2',
25
+ '.wasm': 'application/wasm',
26
+ '.map': 'application/json',
27
+ };
28
+
29
+ function resolveSDKRoot() {
30
+ return path.resolve(__dirname, '..', '..', '..');
31
+ }
32
+
33
+ function resolveCoreDist() {
34
+ const sdkRoot = resolveSDKRoot();
35
+
36
+ const localCore = path.join(sdkRoot, 'node_modules', '@viji-dev', 'core', 'dist');
37
+ if (fs.existsSync(localCore)) return localCore;
38
+
39
+ let dir = sdkRoot;
40
+ while (true) {
41
+ const candidate = path.join(dir, 'node_modules', '@viji-dev', 'core', 'dist');
42
+ if (fs.existsSync(candidate)) return candidate;
43
+ const parent = path.dirname(dir);
44
+ if (parent === dir) break;
45
+ dir = parent;
46
+ }
47
+
48
+ return null;
49
+ }
50
+
51
+ function serveFile(res, filePath) {
52
+ const ext = path.extname(filePath);
53
+ const mime = MIME_TYPES[ext] || 'application/octet-stream';
54
+
55
+ try {
56
+ const data = fs.readFileSync(filePath);
57
+ res.writeHead(200, {
58
+ 'Content-Type': mime,
59
+ 'Cache-Control': 'no-cache',
60
+ 'Access-Control-Allow-Origin': '*',
61
+ });
62
+ res.end(data);
63
+ } catch {
64
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
65
+ res.end('Not Found');
66
+ }
67
+ }
68
+
69
+ function sendJSON(res, data, status = 200) {
70
+ res.writeHead(status, {
71
+ 'Content-Type': 'application/json',
72
+ 'Access-Control-Allow-Origin': '*',
73
+ });
74
+ res.end(JSON.stringify(data));
75
+ }
76
+
77
+ export async function startDevServer({ port, host, open, scenesDir }) {
78
+ const sdkRoot = resolveSDKRoot();
79
+ const distDir = path.join(sdkRoot, 'dist');
80
+ const coreDist = resolveCoreDist();
81
+
82
+ if (!fs.existsSync(distDir) || !fs.existsSync(path.join(distDir, 'index.html'))) {
83
+ console.error('SDK UI not found. The dist/ folder is missing or incomplete.');
84
+ console.error(`Expected at: ${distDir}`);
85
+ process.exit(1);
86
+ }
87
+
88
+ const server = http.createServer(async (req, res) => {
89
+ const url = new URL(req.url, `http://${host}:${port}`);
90
+ const pathname = url.pathname;
91
+
92
+ if (req.method === 'OPTIONS') {
93
+ res.writeHead(204, {
94
+ 'Access-Control-Allow-Origin': '*',
95
+ 'Access-Control-Allow-Methods': 'GET, OPTIONS',
96
+ 'Access-Control-Allow-Headers': 'Content-Type',
97
+ });
98
+ res.end();
99
+ return;
100
+ }
101
+
102
+ if (pathname === '/api/scenes') {
103
+ try {
104
+ const scenes = await scanScenes(scenesDir);
105
+ sendJSON(res, scenes);
106
+ } catch (err) {
107
+ sendJSON(res, { error: err.message }, 500);
108
+ }
109
+ return;
110
+ }
111
+
112
+ const codeMatch = pathname.match(/^\/api\/scenes\/([^/]+)\/code$/);
113
+ if (codeMatch) {
114
+ try {
115
+ const code = await readSceneCode(scenesDir, decodeURIComponent(codeMatch[1]));
116
+ if (code === null) {
117
+ sendJSON(res, { error: 'Scene not found' }, 404);
118
+ } else {
119
+ sendJSON(res, { code });
120
+ }
121
+ } catch (err) {
122
+ sendJSON(res, { error: err.message }, 500);
123
+ }
124
+ return;
125
+ }
126
+
127
+ const sceneMatch = pathname.match(/^\/api\/scenes\/([^/]+)$/);
128
+ if (sceneMatch) {
129
+ try {
130
+ const scene = await scanSingleScene(scenesDir, decodeURIComponent(sceneMatch[1]));
131
+ if (!scene) {
132
+ sendJSON(res, { error: 'Scene not found' }, 404);
133
+ } else {
134
+ sendJSON(res, scene);
135
+ }
136
+ } catch (err) {
137
+ sendJSON(res, { error: err.message }, 500);
138
+ }
139
+ return;
140
+ }
141
+
142
+ if (coreDist && pathname.startsWith('/dist/assets/')) {
143
+ const assetPath = pathname.replace('/dist/assets/', '');
144
+ const fullPath = path.join(coreDist, 'assets', assetPath);
145
+ if (fs.existsSync(fullPath)) {
146
+ serveFile(res, fullPath);
147
+ return;
148
+ }
149
+ }
150
+
151
+ let filePath;
152
+ if (pathname === '/' || pathname === '/index.html') {
153
+ filePath = path.join(distDir, 'index.html');
154
+ } else {
155
+ filePath = path.join(distDir, pathname);
156
+ }
157
+
158
+ if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
159
+ serveFile(res, filePath);
160
+ return;
161
+ }
162
+
163
+ serveFile(res, path.join(distDir, 'index.html'));
164
+ });
165
+
166
+ const wss = new WebSocketServer({ server, path: '/ws' });
167
+ const clients = new Set();
168
+
169
+ wss.on('connection', (ws) => {
170
+ clients.add(ws);
171
+ ws.on('close', () => clients.delete(ws));
172
+ ws.on('error', () => clients.delete(ws));
173
+ });
174
+
175
+ function broadcast(message) {
176
+ const data = JSON.stringify(message);
177
+ for (const ws of clients) {
178
+ if (ws.readyState === 1) {
179
+ ws.send(data);
180
+ }
181
+ }
182
+ }
183
+
184
+ const watcher = chokidar.watch(scenesDir, {
185
+ ignoreInitial: true,
186
+ ignored: /(^|[/\\])\./,
187
+ persistent: true,
188
+ depth: 3,
189
+ });
190
+
191
+ watcher.on('add', async (filePath) => {
192
+ const sceneName = extractSceneName(scenesDir, filePath);
193
+ if (!sceneName) return;
194
+ const scene = await scanSingleScene(scenesDir, sceneName);
195
+ if (scene) broadcast({ type: 'scene:add', scene });
196
+ });
197
+
198
+ watcher.on('change', async (filePath) => {
199
+ const sceneName = extractSceneName(scenesDir, filePath);
200
+ if (!sceneName) return;
201
+ const scene = await scanSingleScene(scenesDir, sceneName);
202
+ if (scene) broadcast({ type: 'scene:update', scene });
203
+ });
204
+
205
+ watcher.on('unlink', (filePath) => {
206
+ const sceneName = extractSceneName(scenesDir, filePath);
207
+ if (!sceneName) return;
208
+ broadcast({ type: 'scene:remove', sceneId: sceneName });
209
+ });
210
+
211
+ return new Promise((resolve) => {
212
+ server.listen(port, host, () => {
213
+ const url = `http://${host}:${port}`;
214
+ console.log(`Viji dev server running at ${url}`);
215
+ console.log(`Watching scenes in: ${scenesDir}`);
216
+ console.log('');
217
+
218
+ if (open) {
219
+ const cmd = process.platform === 'win32' ? 'start' :
220
+ process.platform === 'darwin' ? 'open' : 'xdg-open';
221
+ exec(`${cmd} ${url}`);
222
+ }
223
+
224
+ resolve({ server, wss, watcher });
225
+ });
226
+ });
227
+ }
228
+
229
+ function extractSceneName(scenesDir, filePath) {
230
+ const rel = path.relative(scenesDir, filePath);
231
+ const parts = rel.split(path.sep);
232
+ return parts.length >= 1 ? parts[0] : null;
233
+ }
@@ -0,0 +1,93 @@
1
+ import { readdir, stat, readFile } from 'fs/promises';
2
+ import { join, extname } from 'path';
3
+ import { existsSync } from 'fs';
4
+
5
+ const SCENE_EXTENSIONS = new Set(['.js', '.ts', '.jsx', '.tsx', '.glsl']);
6
+
7
+ function detectRenderer(code, ext) {
8
+ if (ext === '.glsl') return 'shader';
9
+
10
+ const directiveMatch = code.match(/\/\/\s*@renderer\s+(\w+)/);
11
+ if (directiveMatch) return directiveMatch[1];
12
+
13
+ if (/function\s+render\s*\(\s*viji\s*,\s*p5\s*\)/.test(code)) return 'p5';
14
+
15
+ return 'native';
16
+ }
17
+
18
+ export async function scanScenes(scenesDir) {
19
+ if (!existsSync(scenesDir)) return [];
20
+
21
+ const entries = await readdir(scenesDir, { withFileTypes: true });
22
+ const scenes = [];
23
+
24
+ for (const entry of entries) {
25
+ if (!entry.isDirectory()) continue;
26
+
27
+ const sceneDir = join(scenesDir, entry.name);
28
+ const mainFile = await findMainFile(sceneDir);
29
+ if (!mainFile) continue;
30
+
31
+ const mainPath = join(sceneDir, mainFile);
32
+ const fileStat = await stat(mainPath);
33
+ const code = await readFile(mainPath, 'utf-8');
34
+ const ext = extname(mainFile);
35
+
36
+ scenes.push({
37
+ id: entry.name,
38
+ name: entry.name,
39
+ renderer: detectRenderer(code, ext),
40
+ mainFile,
41
+ createdAt: fileStat.birthtime.toISOString(),
42
+ lastModified: fileStat.mtime.toISOString(),
43
+ code,
44
+ });
45
+ }
46
+
47
+ return scenes.sort((a, b) => a.name.localeCompare(b.name));
48
+ }
49
+
50
+ export async function scanSingleScene(scenesDir, sceneName) {
51
+ const sceneDir = join(scenesDir, sceneName);
52
+ if (!existsSync(sceneDir)) return null;
53
+
54
+ const mainFile = await findMainFile(sceneDir);
55
+ if (!mainFile) return null;
56
+
57
+ const mainPath = join(sceneDir, mainFile);
58
+ const fileStat = await stat(mainPath);
59
+ const code = await readFile(mainPath, 'utf-8');
60
+ const ext = extname(mainFile);
61
+
62
+ return {
63
+ id: sceneName,
64
+ name: sceneName,
65
+ renderer: detectRenderer(code, ext),
66
+ mainFile,
67
+ createdAt: fileStat.birthtime.toISOString(),
68
+ lastModified: fileStat.mtime.toISOString(),
69
+ code,
70
+ };
71
+ }
72
+
73
+ async function findMainFile(sceneDir) {
74
+ const candidates = ['main.js', 'main.ts', 'main.jsx', 'main.tsx', 'main.glsl'];
75
+ for (const name of candidates) {
76
+ if (existsSync(join(sceneDir, name))) return name;
77
+ }
78
+
79
+ try {
80
+ const files = await readdir(sceneDir);
81
+ return files.find(f => SCENE_EXTENSIONS.has(extname(f))) ?? null;
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+
87
+ export async function readSceneCode(scenesDir, sceneName) {
88
+ const sceneDir = join(scenesDir, sceneName);
89
+ const mainFile = await findMainFile(sceneDir);
90
+ if (!mainFile) return null;
91
+
92
+ return readFile(join(sceneDir, mainFile), 'utf-8');
93
+ }
@@ -0,0 +1,2 @@
1
+ import type { Plugin } from 'vite';
2
+ export function vijiScenePlugin(): Plugin;
@@ -0,0 +1,134 @@
1
+ import { join, relative, sep } from 'node:path';
2
+ import { WebSocketServer } from 'ws';
3
+ import chokidar from 'chokidar';
4
+ import { scanScenes, scanSingleScene, readSceneCode } from './scene-scanner.js';
5
+
6
+ export function vijiScenePlugin() {
7
+ const scenesDir = join(process.cwd(), 'scenes');
8
+ let wss;
9
+ let watcher;
10
+
11
+ return {
12
+ name: 'viji-scene-server',
13
+
14
+ configureServer(server) {
15
+ const clients = new Set();
16
+
17
+ wss = new WebSocketServer({ noServer: true });
18
+
19
+ server.httpServer.on('upgrade', (req, socket, head) => {
20
+ if (req.url === '/ws') {
21
+ wss.handleUpgrade(req, socket, head, (ws) => {
22
+ wss.emit('connection', ws, req);
23
+ });
24
+ }
25
+ });
26
+
27
+ wss.on('connection', (ws) => {
28
+ clients.add(ws);
29
+ ws.on('close', () => clients.delete(ws));
30
+ ws.on('error', () => clients.delete(ws));
31
+ });
32
+
33
+ function broadcast(message) {
34
+ const data = JSON.stringify(message);
35
+ for (const ws of clients) {
36
+ if (ws.readyState === 1) ws.send(data);
37
+ }
38
+ }
39
+
40
+ watcher = chokidar.watch(scenesDir, {
41
+ ignoreInitial: true,
42
+ ignored: /(^|[/\\])\./,
43
+ persistent: true,
44
+ depth: 3,
45
+ });
46
+
47
+ watcher.on('add', async (filePath) => {
48
+ const sceneName = extractSceneName(scenesDir, filePath);
49
+ if (!sceneName) return;
50
+ const scene = await scanSingleScene(scenesDir, sceneName);
51
+ if (scene) broadcast({ type: 'scene:add', scene });
52
+ });
53
+
54
+ watcher.on('change', async (filePath) => {
55
+ const sceneName = extractSceneName(scenesDir, filePath);
56
+ if (!sceneName) return;
57
+ const scene = await scanSingleScene(scenesDir, sceneName);
58
+ if (scene) broadcast({ type: 'scene:update', scene });
59
+ });
60
+
61
+ watcher.on('unlink', (filePath) => {
62
+ const sceneName = extractSceneName(scenesDir, filePath);
63
+ if (!sceneName) return;
64
+ broadcast({ type: 'scene:remove', sceneId: sceneName });
65
+ });
66
+
67
+ server.middlewares.use(async (req, res, next) => {
68
+ const url = new URL(req.url, `http://${req.headers.host}`);
69
+ const pathname = url.pathname;
70
+
71
+ if (pathname === '/api/scenes') {
72
+ try {
73
+ const scenes = await scanScenes(scenesDir);
74
+ sendJSON(res, scenes);
75
+ } catch (err) {
76
+ sendJSON(res, { error: err.message }, 500);
77
+ }
78
+ return;
79
+ }
80
+
81
+ const codeMatch = pathname.match(/^\/api\/scenes\/([^/]+)\/code$/);
82
+ if (codeMatch) {
83
+ try {
84
+ const code = await readSceneCode(scenesDir, decodeURIComponent(codeMatch[1]));
85
+ if (code === null) {
86
+ sendJSON(res, { error: 'Scene not found' }, 404);
87
+ } else {
88
+ sendJSON(res, { code });
89
+ }
90
+ } catch (err) {
91
+ sendJSON(res, { error: err.message }, 500);
92
+ }
93
+ return;
94
+ }
95
+
96
+ const sceneMatch = pathname.match(/^\/api\/scenes\/([^/]+)$/);
97
+ if (sceneMatch) {
98
+ try {
99
+ const scene = await scanSingleScene(scenesDir, decodeURIComponent(sceneMatch[1]));
100
+ if (!scene) {
101
+ sendJSON(res, { error: 'Scene not found' }, 404);
102
+ } else {
103
+ sendJSON(res, scene);
104
+ }
105
+ } catch (err) {
106
+ sendJSON(res, { error: err.message }, 500);
107
+ }
108
+ return;
109
+ }
110
+
111
+ next();
112
+ });
113
+ },
114
+
115
+ closeBundle() {
116
+ if (watcher) watcher.close();
117
+ if (wss) wss.close();
118
+ },
119
+ };
120
+ }
121
+
122
+ function sendJSON(res, data, status = 200) {
123
+ res.writeHead(status, {
124
+ 'Content-Type': 'application/json',
125
+ 'Access-Control-Allow-Origin': '*',
126
+ });
127
+ res.end(JSON.stringify(data));
128
+ }
129
+
130
+ function extractSceneName(scenesDir, filePath) {
131
+ const rel = relative(scenesDir, filePath);
132
+ const parts = rel.split(sep);
133
+ return parts.length >= 1 ? parts[0] : null;
134
+ }
@@ -1,128 +1,61 @@
1
- /**
2
- * CLI Utilities - Common functions for CLI commands
3
- */
4
-
5
- import { existsSync, readFileSync } from 'fs';
6
- import { join, dirname } from 'path';
1
+ import { existsSync } from 'fs';
2
+ import { join } from 'path';
7
3
 
8
4
  export function validateProject(projectDir = process.cwd()) {
9
- // Support two layouts:
10
- // 1) SDK scene folder: scenes/<project>/main.(js|ts)
11
- // 2) External project: src/main.(js|ts)
12
5
  const candidates = [
13
6
  join(projectDir, 'main.ts'),
14
7
  join(projectDir, 'main.js'),
15
8
  join(projectDir, 'main.tsx'),
16
9
  join(projectDir, 'main.jsx'),
17
- join(projectDir, 'src', 'main.ts'),
18
- join(projectDir, 'src', 'main.js'),
19
- join(projectDir, 'src', 'main.tsx'),
20
- join(projectDir, 'src', 'main.jsx')
10
+ join(projectDir, 'main.glsl'),
21
11
  ];
22
12
 
23
13
  const mainScenePath = candidates.find(p => existsSync(p));
24
- const packageJsonPath = join(projectDir, 'package.json');
25
14
 
26
15
  const issues = [];
27
16
  if (!mainScenePath) {
28
- issues.push('No main.(js|ts|jsx|tsx) found in current folder or src/');
17
+ issues.push('No main.(js|ts|jsx|tsx|glsl) found in scene folder');
29
18
  }
30
19
 
31
20
  return {
32
21
  valid: issues.length === 0,
33
22
  issues,
34
23
  paths: {
35
- packageJson: packageJsonPath,
36
24
  mainScene: mainScenePath || join(projectDir, 'main.js'),
37
- projectDir
38
- }
25
+ projectDir,
26
+ },
39
27
  };
40
28
  }
41
29
 
42
- export function getProjectInfo(projectDir = process.cwd()) {
43
- const validation = validateProject(projectDir);
44
-
45
- if (!validation.valid) {
46
- return { valid: false, ...validation };
47
- }
48
-
49
- try {
50
- const packageJson = JSON.parse(readFileSync(validation.paths.packageJson, 'utf8'));
51
-
52
- return {
53
- valid: true,
54
- name: packageJson.name,
55
- version: packageJson.version,
56
- description: packageJson.description,
57
- dependencies: packageJson.dependencies || {},
58
- devDependencies: packageJson.devDependencies || {},
59
- scripts: packageJson.scripts || {},
60
- paths: validation.paths
61
- };
62
- } catch (error) {
63
- return {
64
- valid: false,
65
- issues: ['Could not read project information'],
66
- error: error.message
67
- };
68
- }
69
- }
70
-
71
- export function formatProjectPath(projectName, targetDir = process.cwd()) {
72
- return join(targetDir, projectName);
73
- }
74
-
75
- export function generateProjectId() {
76
- return Math.random().toString(36).substr(2, 9);
77
- }
78
-
79
30
  export function validateProjectName(name) {
80
31
  const issues = [];
81
-
32
+
82
33
  if (!name || name.trim().length === 0) {
83
- issues.push('Project name cannot be empty');
34
+ issues.push('Scene name cannot be empty');
84
35
  }
85
-
86
- if (name.length > 214) {
87
- issues.push('Project name too long (max 214 characters)');
36
+
37
+ if (name && name.length > 214) {
38
+ issues.push('Scene name too long (max 214 characters)');
88
39
  }
89
-
90
- if (name.toLowerCase() !== name) {
91
- issues.push('Project name should be lowercase');
40
+
41
+ if (name && name.toLowerCase() !== name) {
42
+ issues.push('Scene name should be lowercase');
92
43
  }
93
-
94
- if (!/^[a-z0-9-_]+$/.test(name)) {
95
- issues.push('Project name can only contain lowercase letters, numbers, hyphens, and underscores');
44
+
45
+ if (name && !/^[a-z0-9-_]+$/.test(name)) {
46
+ issues.push('Scene name can only contain lowercase letters, numbers, hyphens, and underscores');
96
47
  }
97
-
98
- if (name.startsWith('.') || name.startsWith('-') || name.startsWith('_')) {
99
- issues.push('Project name cannot start with a dot, hyphen, or underscore');
48
+
49
+ if (name && (name.startsWith('.') || name.startsWith('-') || name.startsWith('_'))) {
50
+ issues.push('Scene name cannot start with a dot, hyphen, or underscore');
100
51
  }
101
-
102
- const reservedNames = ['viji', 'viji-sdk', 'core', 'main', 'index', 'src', 'dist', 'build'];
103
- if (reservedNames.includes(name.toLowerCase())) {
104
- issues.push(`Project name "${name}" is reserved`);
52
+
53
+ const reserved = ['viji', 'viji-sdk', 'core', 'main', 'index', 'src', 'dist', 'build', 'node_modules'];
54
+ if (name && reserved.includes(name.toLowerCase())) {
55
+ issues.push(`Scene name "${name}" is reserved`);
105
56
  }
106
-
107
- return {
108
- valid: issues.length === 0,
109
- issues
110
- };
111
- }
112
57
 
113
- export function logWithIcon(icon, message, color = 'white') {
114
- const colors = {
115
- red: '\x1b[31m',
116
- green: '\x1b[32m',
117
- yellow: '\x1b[33m',
118
- blue: '\x1b[34m',
119
- magenta: '\x1b[35m',
120
- cyan: '\x1b[36m',
121
- white: '\x1b[37m',
122
- reset: '\x1b[0m'
123
- };
124
-
125
- console.log(`${colors[color]}${icon} ${message}${colors.reset}`);
58
+ return { valid: issues.length === 0, issues };
126
59
  }
127
60
 
128
61
  export function formatDuration(ms) {
@@ -133,20 +66,18 @@ export function formatDuration(ms) {
133
66
 
134
67
  export function formatFileSize(bytes) {
135
68
  if (bytes === 0) return '0 B';
136
-
137
69
  const k = 1024;
138
70
  const sizes = ['B', 'KB', 'MB', 'GB'];
139
71
  const i = Math.floor(Math.log(bytes) / Math.log(k));
140
-
141
72
  return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
142
73
  }
143
74
 
144
75
  export function createSpinner(text = 'Loading...') {
145
- const frames = ['', '', '', '', '', '', '', '', '', ''];
76
+ const frames = ['\u28CB', '\u28D9', '\u28F9', '\u28F8', '\u28FC', '\u28F4', '\u28E6', '\u28E7', '\u28C7', '\u28CF'];
146
77
  let i = 0;
147
78
  let intervalId;
148
-
149
- const spinner = {
79
+
80
+ return {
150
81
  start() {
151
82
  process.stdout.write(`${frames[0]} ${text}`);
152
83
  intervalId = setInterval(() => {
@@ -154,55 +85,14 @@ export function createSpinner(text = 'Loading...') {
154
85
  i = (i + 1) % frames.length;
155
86
  }, 80);
156
87
  },
157
-
158
88
  stop(finalText) {
159
89
  if (intervalId) {
160
90
  clearInterval(intervalId);
161
91
  process.stdout.write(`\r${finalText || text}\n`);
162
92
  }
163
93
  },
164
-
165
94
  update(newText) {
166
95
  text = newText;
167
- }
96
+ },
168
97
  };
169
-
170
- return spinner;
171
- }
172
-
173
- export function isVijiWorkspace(dir = process.cwd()) {
174
- // Check for key workspace files
175
- const requiredFiles = [
176
- join(dir, 'src'),
177
- join(dir, 'scenes'),
178
- join(dir, 'vite.config.ts'),
179
- join(dir, 'package.json')
180
- ];
181
-
182
- // All required files must exist
183
- if (!requiredFiles.every(file => existsSync(file))) {
184
- return false;
185
- }
186
-
187
- // Check if package.json has viji workspace marker
188
- try {
189
- const packageJson = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
190
- return packageJson.viji && packageJson.viji.workspaceVersion;
191
- } catch (error) {
192
- return false;
193
- }
194
98
  }
195
-
196
- export function findWorkspaceRoot(startDir = process.cwd()) {
197
- let currentDir = startDir;
198
-
199
- while (currentDir !== dirname(currentDir)) {
200
- if (isVijiWorkspace(currentDir)) {
201
- return currentDir;
202
- }
203
- currentDir = dirname(currentDir);
204
- }
205
-
206
- return null;
207
- }
208
-