cyrus-studio 1.0.7 → 1.1.0

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 (34) hide show
  1. package/README.md +34 -1
  2. package/app/assets/AuthDialog-Dry3B6Q0.js +1 -0
  3. package/app/assets/CanvaSaveModal-BOOuwAeY.js +1 -0
  4. package/app/assets/CodingWorkspace-IGR61s5_.js +4 -0
  5. package/app/assets/CreateAppDialog-DrI4Lkd8.js +1236 -0
  6. package/app/assets/CreateProjectDialog-DI7tlDTn.js +1 -0
  7. package/app/assets/HistoryTimelineDialog-IxHPdvMy.js +1 -0
  8. package/app/assets/ImportModal-C7-xg0X4.js +83 -0
  9. package/app/assets/KeyboardShortcutsDialog-BToCWj6j.js +1 -0
  10. package/app/assets/LiveRuntimeWorkspace-BgtwTTvN.js +1 -0
  11. package/app/assets/ShareExportDialog-Dp3sb0kP.js +1 -0
  12. package/app/assets/UserProfileDialog-CIISMYby.js +1 -0
  13. package/app/assets/WelcomeOnboardingModal-B-_unbCE.js +1 -0
  14. package/app/assets/exportZip-JYUImV2j.js +1 -0
  15. package/app/assets/index-DD12pOJ9.css +2 -0
  16. package/app/assets/index-DKH4UEV-.js +2205 -0
  17. package/app/assets/rolldown-runtime-W7wSyTde.js +1 -0
  18. package/app/assets/themes-lx_odDI8.js +1 -0
  19. package/app/assets/vendor-firebase-DNMzpOGz.js +32 -0
  20. package/app/assets/vendor-icons-CmYui5CW.js +13 -0
  21. package/app/assets/vendor-jszip-Cv4gVObP.js +2 -0
  22. package/app/assets/vendor-react-B3_iTqTw.js +227 -0
  23. package/app/assets/vendor-utils-D92Kf24_.js +1 -0
  24. package/app/favicon.svg +1 -0
  25. package/app/icons.svg +24 -0
  26. package/app/index.html +50 -0
  27. package/app/login-bg.png +0 -0
  28. package/app/test-sdk-sandbox.html +720 -0
  29. package/bin/dev-server.mjs +692 -0
  30. package/bin/htmlParser.mjs +337 -0
  31. package/dist/index.cjs +1167 -36
  32. package/dist/index.d.ts +337 -9
  33. package/dist/index.mjs +1167 -36
  34. package/package.json +22 -4
@@ -0,0 +1,692 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Cyrus Studio — Universal 1-Command Dev Bridge Server
5
+ *
6
+ * Serves the standalone local Cyrus visual studio environment on localhost
7
+ * and syncs all canvas edits directly to local project files in real time.
8
+ * Zero external cloud dependencies required.
9
+ */
10
+
11
+ import http from 'node:http';
12
+ import crypto from 'node:crypto';
13
+ import fs from 'node:fs';
14
+ import path from 'node:path';
15
+ import os from 'node:os';
16
+ import { exec } from 'node:child_process';
17
+ import { fileURLToPath } from 'node:url';
18
+ import { parseHtmlToCyrusProject } from './htmlParser.mjs';
19
+
20
+ // ─── RESOLVE APP DIRECTORY ──────────────────────────────────────────
21
+ const __filename = fileURLToPath(import.meta.url);
22
+ const __dirname = path.dirname(__filename);
23
+
24
+ // Check both packages/cyrus-sdk/app and root dist/
25
+ let appDir = path.resolve(__dirname, '../app');
26
+ if (!fs.existsSync(path.join(appDir, 'index.html'))) {
27
+ const rootDist = path.resolve(__dirname, '../../../dist');
28
+ if (fs.existsSync(path.join(rootDist, 'index.html'))) {
29
+ appDir = rootDist;
30
+ }
31
+ }
32
+
33
+ // ─── CONFIG & ENVIRONMENT DETECTION ─────────────────────────────────
34
+ const DB_DIR = path.join(os.homedir(), '.cyrus-studio');
35
+ const CONFIG_FILE = path.join(DB_DIR, 'config.json');
36
+ const LOCAL_CONFIG_DIR = path.join(process.cwd(), '.cyrus');
37
+ const LOCAL_CONFIG_FILE = path.join(LOCAL_CONFIG_DIR, 'config.json');
38
+ const PROJECT_SAVE_FILE = path.join(process.cwd(), '.cyrus', 'project.json');
39
+ const GLOBAL_PROJECT_SAVE_FILE = path.join(os.homedir(), '.cyrus-studio', 'last-project.json');
40
+
41
+ const DEFAULT_CONFIG = {
42
+ theme: 'dark',
43
+ framework: 'auto',
44
+ };
45
+
46
+ function loadCyrusConfig() {
47
+ let config = { ...DEFAULT_CONFIG };
48
+ try {
49
+ if (fs.existsSync(CONFIG_FILE)) {
50
+ const globalCfg = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
51
+ config = { ...config, ...globalCfg };
52
+ }
53
+ } catch {}
54
+
55
+ try {
56
+ if (fs.existsSync(LOCAL_CONFIG_FILE)) {
57
+ const localCfg = JSON.parse(fs.readFileSync(LOCAL_CONFIG_FILE, 'utf8'));
58
+ config = { ...config, ...localCfg };
59
+ }
60
+ } catch {}
61
+
62
+ return config;
63
+ }
64
+
65
+ // ─── PROJECT STATE PERSISTENCE ───────────────────────────────────────
66
+ /** Load the saved full project JSON from disk. Checks local .cyrus/ first, then global ~/.cyrus-studio/ */
67
+ function loadSavedProject() {
68
+ // 1. Try local project-specific save
69
+ if (fs.existsSync(PROJECT_SAVE_FILE)) {
70
+ try {
71
+ const raw = fs.readFileSync(PROJECT_SAVE_FILE, 'utf8');
72
+ const project = JSON.parse(raw);
73
+ if (project && project.id && project.pages) {
74
+ return project;
75
+ }
76
+ } catch {}
77
+ }
78
+ // 2. Fallback to global last-used save
79
+ if (fs.existsSync(GLOBAL_PROJECT_SAVE_FILE)) {
80
+ try {
81
+ const raw = fs.readFileSync(GLOBAL_PROJECT_SAVE_FILE, 'utf8');
82
+ const project = JSON.parse(raw);
83
+ if (project && project.id && project.pages) {
84
+ return project;
85
+ }
86
+ } catch {}
87
+ }
88
+ return null;
89
+ }
90
+
91
+ /** Save the full project JSON to .cyrus/project.json and the global fallback */
92
+ function saveProjectToDisk(project) {
93
+ try {
94
+ const localDir = path.dirname(PROJECT_SAVE_FILE);
95
+ if (!fs.existsSync(localDir)) fs.mkdirSync(localDir, { recursive: true });
96
+ fs.writeFileSync(PROJECT_SAVE_FILE, JSON.stringify(project, null, 2), 'utf8');
97
+ } catch (err) {
98
+ // Non-fatal: silently ignore write errors
99
+ }
100
+ try {
101
+ const globalDir = path.dirname(GLOBAL_PROJECT_SAVE_FILE);
102
+ if (!fs.existsSync(globalDir)) fs.mkdirSync(globalDir, { recursive: true });
103
+ fs.writeFileSync(GLOBAL_PROJECT_SAVE_FILE, JSON.stringify(project, null, 2), 'utf8');
104
+ } catch {}
105
+ }
106
+
107
+
108
+
109
+ // ─── AUTOMATIC FRAMEWORK DETECTION ───────────────────────────────────
110
+ function detectProjectFramework() {
111
+ const cwd = process.cwd();
112
+
113
+ if (fs.existsSync(path.join(cwd, 'pubspec.yaml'))) return 'flutter';
114
+ if (fs.existsSync(path.join(cwd, 'Package.swift'))) return 'swiftui';
115
+
116
+ try {
117
+ const files = fs.readdirSync(cwd);
118
+ if (files.some(f => f.endsWith('.xcodeproj') || f.endsWith('.xcworkspace'))) return 'swiftui';
119
+ } catch {}
120
+
121
+ if (fs.existsSync(path.join(cwd, 'package.json'))) {
122
+ try {
123
+ const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
124
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
125
+ if (deps['next'] || deps['react'] || deps['vite'] || deps['tailwindcss']) return 'react';
126
+ if (deps['vue'] || deps['svelte']) return 'html';
127
+ } catch {}
128
+ return 'react';
129
+ }
130
+
131
+ try {
132
+ const files = fs.readdirSync(cwd);
133
+ if (files.some(f => f.endsWith('.html') || f.endsWith('.htm'))) {
134
+ return 'html';
135
+ }
136
+ } catch {}
137
+
138
+ return 'react';
139
+ }
140
+
141
+ // ─── CLI ARGUMENTS ──────────────────────────────────────────────────
142
+ const rawArgs = process.argv.slice(2);
143
+ const args = rawArgs.filter(a => a !== 'dev' && a !== 'start' && a !== 'open');
144
+
145
+ const getArg = (key) => {
146
+ const match = args.find(a => a.startsWith(`--${key}=`));
147
+ if (match) return match.split('=').slice(1).join('=');
148
+ const index = args.indexOf(`--${key}`);
149
+ if (index !== -1 && args[index + 1]) return args[index + 1];
150
+ return null;
151
+ };
152
+
153
+ const config = loadCyrusConfig();
154
+ const detectedFramework = detectProjectFramework();
155
+
156
+ // Positional file argument support (e.g. `cyrus luck.html` or `cyrus ./src/App.tsx`)
157
+ const positionalFile = args.find(a => !a.startsWith('-') && (a.includes('.') || fs.existsSync(path.resolve(process.cwd(), a))));
158
+ const outPath = getArg('out') || positionalFile || null;
159
+
160
+ // Infer framework from file extension if target file is provided
161
+ function inferFrameworkFromFile(file) {
162
+ if (!file) return null;
163
+ const ext = path.extname(file).toLowerCase();
164
+ if (ext === '.html' || ext === '.htm') return 'html';
165
+ if (ext === '.dart') return 'flutter';
166
+ if (ext === '.swift') return 'swiftui';
167
+ if (ext === '.tsx' || ext === '.jsx' || ext === '.js' || ext === '.ts') return 'react';
168
+ return null;
169
+ }
170
+
171
+ const inferredFw = inferFrameworkFromFile(outPath);
172
+ const framework = getArg('framework') || inferredFw || (config.framework !== 'auto' ? config.framework : detectedFramework) || 'react';
173
+ const port = parseInt(getArg('port') || '4799', 10);
174
+ const projectId = getArg('project') || 'env';
175
+ const theme = getArg('theme') || config.theme || 'dark';
176
+ const noOpen = args.includes('--no-open');
177
+
178
+ // ─── RESOLVE OUTPUT FILES ───────────────────────────────────────────
179
+ function getOutputFiles() {
180
+ const cwd = process.cwd();
181
+
182
+ if (outPath) {
183
+ return { [framework]: path.resolve(cwd, outPath) };
184
+ }
185
+
186
+ const outputs = {};
187
+ const hasSrc = fs.existsSync(path.join(cwd, 'src'));
188
+ const hasLib = fs.existsSync(path.join(cwd, 'lib'));
189
+ const hasApp = fs.existsSync(path.join(cwd, 'app'));
190
+
191
+ if (framework === 'react') {
192
+ if (hasSrc) outputs.react = path.join(cwd, 'src', 'CyrusApp.tsx');
193
+ else if (hasApp) outputs.react = path.join(cwd, 'app', 'CyrusApp.tsx');
194
+ else outputs.react = path.join(cwd, 'App.jsx');
195
+ } else if (framework === 'flutter') {
196
+ outputs.flutter = hasLib ? path.join(cwd, 'lib', 'cyrus_ui.dart') : path.join(cwd, 'cyrus_ui.dart');
197
+ } else if (framework === 'swiftui') {
198
+ outputs.swiftui = path.join(cwd, 'ContentView.swift');
199
+ } else if (framework === 'html') {
200
+ if (fs.existsSync(path.join(cwd, 'index.html'))) {
201
+ outputs.html = path.join(cwd, 'index.html');
202
+ } else {
203
+ try {
204
+ const files = fs.readdirSync(cwd);
205
+ const firstHtml = files.find(f => f.endsWith('.html') || f.endsWith('.htm'));
206
+ outputs.html = firstHtml ? path.join(cwd, firstHtml) : path.join(cwd, 'index.html');
207
+ } catch {
208
+ outputs.html = path.join(cwd, 'index.html');
209
+ }
210
+ }
211
+ }
212
+
213
+ const cyrusDir = path.join(cwd, '.cyrus');
214
+ if (!fs.existsSync(cyrusDir)) fs.mkdirSync(cyrusDir, { recursive: true });
215
+
216
+ return {
217
+ ...outputs,
218
+ react: outputs.react || path.join(cyrusDir, 'App.jsx'),
219
+ html: outputs.html || path.join(cyrusDir, 'index.html'),
220
+ flutter: outputs.flutter || path.join(cyrusDir, 'app.dart'),
221
+ swiftui: outputs.swiftui || path.join(cyrusDir, 'ContentView.swift'),
222
+ };
223
+ }
224
+
225
+ // ─── MIME TYPES ─────────────────────────────────────────────────────
226
+ const MIME_TYPES = {
227
+ '.html': 'text/html; charset=utf-8',
228
+ '.js': 'application/javascript; charset=utf-8',
229
+ '.mjs': 'application/javascript; charset=utf-8',
230
+ '.css': 'text/css; charset=utf-8',
231
+ '.json': 'application/json; charset=utf-8',
232
+ '.svg': 'image/svg+xml',
233
+ '.png': 'image/png',
234
+ '.jpg': 'image/jpeg',
235
+ '.jpeg': 'image/jpeg',
236
+ '.ico': 'image/x-icon',
237
+ '.woff': 'font/woff',
238
+ '.woff2':'font/woff2',
239
+ '.ttf': 'font/ttf',
240
+ };
241
+
242
+ // ─── FILE SYNC ENGINE ───────────────────────────────────────────────
243
+ function writeCodeFiles(payload) {
244
+ const outputFiles = getOutputFiles();
245
+ const written = [];
246
+
247
+ for (const [fw, filePath] of Object.entries(outputFiles)) {
248
+ const code = payload[fw];
249
+ if (!code) continue;
250
+ if (outPath && fw !== framework) continue;
251
+
252
+ try {
253
+ const dir = path.dirname(filePath);
254
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
255
+ fs.writeFileSync(filePath, code, 'utf8');
256
+ written.push(filePath);
257
+ } catch (err) {
258
+ console.error(` ✗ Failed to write ${filePath}:`, err.message);
259
+ }
260
+ }
261
+
262
+ if (written.length > 0) {
263
+ console.log(` \x1b[32m✓\x1b[0m ${new Date().toLocaleTimeString()} — Synced visual edits to disk:`);
264
+ written.forEach(f => console.log(` \x1b[36m${path.relative(process.cwd(), f)}\x1b[0m`));
265
+ }
266
+ }
267
+
268
+ // ─── ZERO-DEPENDENCY RFC 6455 WEBSOCKET ENGINE ───────────────────────
269
+ const wsClients = new Set();
270
+
271
+ function broadcastWs(msgObj) {
272
+ const data = JSON.stringify(msgObj);
273
+ const payload = Buffer.from(data, 'utf8');
274
+ const len = payload.length;
275
+
276
+ let header;
277
+ if (len < 126) {
278
+ header = Buffer.from([0x81, len]);
279
+ } else if (len <= 0xFFFF) {
280
+ header = Buffer.alloc(4);
281
+ header[0] = 0x81;
282
+ header[1] = 126;
283
+ header.writeUInt16BE(len, 2);
284
+ } else {
285
+ header = Buffer.alloc(10);
286
+ header[0] = 0x81;
287
+ header[1] = 127;
288
+ header.writeBigUInt64BE(BigInt(len), 2);
289
+ }
290
+
291
+ const frame = Buffer.concat([header, payload]);
292
+ for (const client of wsClients) {
293
+ try { client.socket.write(frame); } catch {}
294
+ }
295
+ }
296
+
297
+ function handleWsUpgrade(req, socket, head) {
298
+ const key = req.headers['sec-websocket-key'];
299
+ if (!key) {
300
+ socket.destroy();
301
+ return;
302
+ }
303
+
304
+ const acceptKey = crypto.createHash('sha1')
305
+ .update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
306
+ .digest('base64');
307
+
308
+ const headers = [
309
+ 'HTTP/1.1 101 Switching Protocols',
310
+ 'Upgrade: websocket',
311
+ 'Connection: Upgrade',
312
+ `Sec-WebSocket-Accept: ${acceptKey}`,
313
+ '', ''
314
+ ];
315
+ socket.write(headers.join('\r\n'));
316
+
317
+ const client = { socket };
318
+ wsClients.add(client);
319
+ console.log(` \x1b[32m●\x1b[0m Canvas connected to live disk sync bridge`);
320
+
321
+ // ── Immediately restore last saved project state to this canvas ──
322
+ const savedProject = loadSavedProject();
323
+ if (savedProject) {
324
+ try {
325
+ const restoreMsg = JSON.stringify({ type: 'RESTORE_PROJECT', payload: savedProject });
326
+ const restorePayload = Buffer.from(restoreMsg, 'utf8');
327
+ const len = restorePayload.length;
328
+ let header;
329
+ if (len < 126) {
330
+ header = Buffer.from([0x81, len]);
331
+ } else if (len <= 0xFFFF) {
332
+ header = Buffer.alloc(4);
333
+ header[0] = 0x81; header[1] = 126;
334
+ header.writeUInt16BE(len, 2);
335
+ } else {
336
+ header = Buffer.alloc(10);
337
+ header[0] = 0x81; header[1] = 127;
338
+ header.writeBigUInt64BE(BigInt(len), 2);
339
+ }
340
+ socket.write(Buffer.concat([header, restorePayload]));
341
+ } catch {}
342
+ }
343
+
344
+ let buffer = Buffer.alloc(0);
345
+
346
+ socket.on('data', (chunk) => {
347
+ buffer = Buffer.concat([buffer, chunk]);
348
+ while (buffer.length >= 2) {
349
+ const byte1 = buffer[0];
350
+ const byte2 = buffer[1];
351
+ const opcode = byte1 & 0x0f;
352
+ const isMasked = (byte2 & 0x80) !== 0;
353
+ let payloadLength = byte2 & 0x7f;
354
+ let offset = 2;
355
+
356
+ if (payloadLength === 126) {
357
+ if (buffer.length < offset + 2) break;
358
+ payloadLength = buffer.readUInt16BE(offset);
359
+ offset += 2;
360
+ } else if (payloadLength === 127) {
361
+ if (buffer.length < offset + 8) break;
362
+ payloadLength = Number(buffer.readBigUInt64BE(offset));
363
+ offset += 8;
364
+ }
365
+
366
+ let mask = null;
367
+ if (isMasked) {
368
+ if (buffer.length < offset + 4) break;
369
+ mask = buffer.subarray(offset, offset + 4);
370
+ offset += 4;
371
+ }
372
+
373
+ if (buffer.length < offset + payloadLength) break;
374
+
375
+ const payload = buffer.subarray(offset, offset + payloadLength);
376
+ buffer = buffer.subarray(offset + payloadLength);
377
+
378
+ if (isMasked && mask) {
379
+ for (let i = 0; i < payload.length; i++) {
380
+ payload[i] ^= mask[i % 4];
381
+ }
382
+ }
383
+
384
+ if (opcode === 0x8) {
385
+ socket.end();
386
+ break;
387
+ }
388
+
389
+ if (opcode === 0x1) {
390
+ try {
391
+ const text = payload.toString('utf8');
392
+ const msg = JSON.parse(text);
393
+
394
+ if (msg.type === 'CODE_SYNC') {
395
+ writeCodeFiles(msg.payload);
396
+ }
397
+
398
+ // Full project state save — persists canvas between sessions
399
+ if (msg.type === 'SAVE_PROJECT' && msg.payload && msg.payload.id) {
400
+ saveProjectToDisk(msg.payload);
401
+ }
402
+
403
+ if (msg.type === 'STATE_CHANGED') {
404
+ const stateFile = path.resolve(process.cwd(), '.cyrus', 'state.json');
405
+ try {
406
+ if (!fs.existsSync(path.dirname(stateFile))) {
407
+ fs.mkdirSync(path.dirname(stateFile), { recursive: true });
408
+ }
409
+ fs.writeFileSync(stateFile, JSON.stringify(msg.payload, null, 2), 'utf8');
410
+ } catch {}
411
+ }
412
+ } catch {}
413
+ }
414
+ }
415
+ });
416
+
417
+ socket.on('close', () => {
418
+ wsClients.delete(client);
419
+ console.log(' ○ Canvas disconnected from bridge');
420
+ });
421
+
422
+ socket.on('error', () => {
423
+ wsClients.delete(client);
424
+ });
425
+ }
426
+
427
+
428
+ // ─── PARSE EXISTING CODE FROM DISK ON STARTUP ───────────────────────
429
+ function getInitialProjectFromDisk() {
430
+ // 1. Check if we have a persisted project from a previous session
431
+ const saved = loadSavedProject();
432
+ if (saved) return saved;
433
+
434
+ // 2. Parse from disk target file (e.g. HTML or React component)
435
+ const outputs = getOutputFiles();
436
+ const targetFile = outPath ? path.resolve(process.cwd(), outPath) : (outputs.html || outputs.react);
437
+
438
+ if (targetFile && fs.existsSync(targetFile)) {
439
+ try {
440
+ const content = fs.readFileSync(targetFile, 'utf8');
441
+ if (content.trim()) {
442
+ const ext = path.extname(targetFile).toLowerCase();
443
+ if (ext === '.html' || ext === '.htm') {
444
+ return parseHtmlToCyrusProject(content, path.basename(targetFile));
445
+ }
446
+ }
447
+ } catch (err) {
448
+ console.error(' ✗ Error parsing existing file from disk:', err.message);
449
+ }
450
+ }
451
+ return null;
452
+ }
453
+
454
+ // ─── START LOCAL SDK HTTP SERVER ────────────────────────────────────
455
+ function startServer() {
456
+ const httpServer = http.createServer((req, res) => {
457
+ const parsedUrl = new URL(req.url, 'http://localhost');
458
+ let pathname = decodeURIComponent(parsedUrl.pathname);
459
+
460
+ // API Initial project from disk
461
+ if (pathname === '/api/initial-project') {
462
+ const project = getInitialProjectFromDisk();
463
+ res.writeHead(200, { 'Content-Type': 'application/json' });
464
+ res.end(JSON.stringify(project || {}));
465
+ return;
466
+ }
467
+
468
+ // API: serve the raw source file content for iframe rendering
469
+ if (pathname === '/api/raw-file') {
470
+ const outputs = getOutputFiles();
471
+ const targetFile = outPath ? path.resolve(process.cwd(), outPath) : (outputs.html || outputs.react);
472
+ if (targetFile && fs.existsSync(targetFile)) {
473
+ try {
474
+ let content = fs.readFileSync(targetFile, 'utf8');
475
+ // Inject live-reload script so the canvas iframe auto-refreshes on disk changes
476
+ const liveReloadScript = `<script>
477
+ (function(){
478
+ var lastCheck = Date.now();
479
+ setInterval(function(){
480
+ fetch('/api/file-mtime').then(r=>r.json()).then(function(d){
481
+ if(d.mtime && d.mtime > lastCheck){ lastCheck = d.mtime; location.reload(); }
482
+ }).catch(function(){});
483
+ }, 800);
484
+ })();
485
+ </script>`;
486
+ content = content.replace('</body>', liveReloadScript + '</body>') ||
487
+ content + liveReloadScript;
488
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'X-Frame-Options': 'SAMEORIGIN' });
489
+ res.end(content);
490
+ } catch (err) {
491
+ res.writeHead(500);
492
+ res.end('Error reading file: ' + err.message);
493
+ }
494
+ } else {
495
+ res.writeHead(404);
496
+ res.end('No target file found');
497
+ }
498
+ return;
499
+ }
500
+
501
+ // API: file modification time for live-reload
502
+ if (pathname === '/api/file-mtime') {
503
+ const outputs = getOutputFiles();
504
+ const targetFile = outPath ? path.resolve(process.cwd(), outPath) : (outputs.html || outputs.react);
505
+ if (targetFile && fs.existsSync(targetFile)) {
506
+ const stat = fs.statSync(targetFile);
507
+ res.writeHead(200, { 'Content-Type': 'application/json' });
508
+ res.end(JSON.stringify({ mtime: stat.mtimeMs, file: path.basename(targetFile) }));
509
+ } else {
510
+ res.writeHead(200, { 'Content-Type': 'application/json' });
511
+ res.end(JSON.stringify({ mtime: 0 }));
512
+ }
513
+ return;
514
+ }
515
+
516
+ // API: file info (name, framework, path)
517
+ if (pathname === '/api/file-info') {
518
+ const outputs = getOutputFiles();
519
+ const targetFile = outPath ? path.resolve(process.cwd(), outPath) : (outputs.html || outputs.react);
520
+ res.writeHead(200, { 'Content-Type': 'application/json' });
521
+ res.end(JSON.stringify({
522
+ file: targetFile ? path.basename(targetFile) : null,
523
+ path: targetFile || null,
524
+ framework,
525
+ hasFile: !!(targetFile && fs.existsSync(targetFile)),
526
+ }));
527
+ return;
528
+ }
529
+
530
+ // API Health check
531
+ if (pathname === '/health') {
532
+ res.writeHead(200, { 'Content-Type': 'application/json' });
533
+ res.end(JSON.stringify({ status: 'ok', framework, version: '1.1.0' }));
534
+ return;
535
+ }
536
+
537
+ // Default root path: Serve Cyrus SPA (which routes to /#/embed/env)
538
+ if (pathname === '/' || pathname === '/index.html' || pathname === '') {
539
+ pathname = '/index.html';
540
+ }
541
+
542
+ const filePath = path.join(appDir, pathname);
543
+
544
+ // Security check: stay within appDir
545
+ if (!filePath.startsWith(appDir)) {
546
+ res.writeHead(403);
547
+ res.end('Forbidden');
548
+ return;
549
+ }
550
+
551
+ if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
552
+ const ext = path.extname(filePath).toLowerCase();
553
+ const contentType = MIME_TYPES[ext] || 'application/octet-stream';
554
+
555
+ // If serving index.html, inject the WebSocket bridge script and initial project
556
+ if (ext === '.html') {
557
+ let html = fs.readFileSync(filePath, 'utf8');
558
+ const initialProject = getInitialProjectFromDisk();
559
+ const initialProjectScript = initialProject ? `\n<script>window.__CYRUS_INITIAL_PROJECT__ = ${JSON.stringify(initialProject)};</script>` : '';
560
+ const bridgeScript = `
561
+ <script>
562
+ (function() {
563
+ try {
564
+ var ws = new WebSocket('ws://' + window.location.host + '/');
565
+ window.__CYRUS_DEV_WS__ = ws;
566
+ ws.onmessage = function(e) {
567
+ try {
568
+ var msg = JSON.parse(e.data);
569
+ if (msg.type === 'RESTORE_PROJECT' && msg.payload) {
570
+ window.dispatchEvent(new CustomEvent('cyrus-restore-project', { detail: msg.payload }));
571
+ }
572
+ } catch(err) {}
573
+ };
574
+ window.addEventListener('message', function(e) {
575
+ if (e.data && e.data.type === 'CYRUS_CODE_SYNC' && ws.readyState === WebSocket.OPEN) {
576
+ ws.send(JSON.stringify({ type: 'CODE_SYNC', payload: e.data.payload }));
577
+ }
578
+ });
579
+ } catch(err) {}
580
+ })();
581
+ </script>`;
582
+ if (html.includes('</head>')) {
583
+ html = html.replace('</head>', initialProjectScript + '</head>');
584
+ } else {
585
+ html = initialProjectScript + html;
586
+ }
587
+ html = html.replace('</body>', bridgeScript + '</body>');
588
+ res.writeHead(200, { 'Content-Type': contentType });
589
+ res.end(html);
590
+ return;
591
+ }
592
+
593
+ res.writeHead(200, { 'Content-Type': contentType });
594
+ fs.createReadStream(filePath).pipe(res);
595
+ } else {
596
+ // Fallback for SPA routing to index.html
597
+ const indexFile = path.join(appDir, 'index.html');
598
+ if (fs.existsSync(indexFile)) {
599
+ let html = fs.readFileSync(indexFile, 'utf8');
600
+ const initialProject = getInitialProjectFromDisk();
601
+ const initialProjectScript = initialProject ? `\n<script>window.__CYRUS_INITIAL_PROJECT__ = ${JSON.stringify(initialProject)};</script>` : '';
602
+ const bridgeScript = `
603
+ <script>
604
+ (function() {
605
+ try {
606
+ var ws = new WebSocket('ws://' + window.location.host + '/');
607
+ window.__CYRUS_DEV_WS__ = ws;
608
+ ws.onmessage = function(e) {
609
+ try {
610
+ var msg = JSON.parse(e.data);
611
+ if (msg.type === 'RESTORE_PROJECT' && msg.payload) {
612
+ window.dispatchEvent(new CustomEvent('cyrus-restore-project', { detail: msg.payload }));
613
+ }
614
+ } catch(err) {}
615
+ };
616
+ window.addEventListener('message', function(e) {
617
+ if (e.data && e.data.type === 'CYRUS_CODE_SYNC' && ws.readyState === WebSocket.OPEN) {
618
+ ws.send(JSON.stringify({ type: 'CODE_SYNC', payload: e.data.payload }));
619
+ }
620
+ });
621
+ } catch(err) {}
622
+ })();
623
+ </script>`;
624
+ if (html.includes('</head>')) {
625
+ html = html.replace('</head>', initialProjectScript + '</head>');
626
+ } else {
627
+ html = initialProjectScript + html;
628
+ }
629
+ html = html.replace('</body>', bridgeScript + '</body>');
630
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
631
+ res.end(html);
632
+ } else {
633
+ res.writeHead(404);
634
+ res.end('Not found');
635
+ }
636
+ }
637
+ });
638
+
639
+ httpServer.on('upgrade', handleWsUpgrade);
640
+
641
+ function tryListen(currentPort, attemptsLeft = 5) {
642
+ httpServer.listen(currentPort, '127.0.0.1', () => {
643
+ const localUrl = `http://localhost:${currentPort}`;
644
+ const embedUrl = localUrl;
645
+ const outputFiles = getOutputFiles();
646
+
647
+ console.log(`
648
+ ╔════════════════════════════════════════════════════════════════════╗
649
+ ║ 🎨 CYRUS STUDIO — STANDALONE LOCAL SDK ║
650
+ ║ Visual Canvas & Instant Bi-directional Code Sync ║
651
+ ╚════════════════════════════════════════════════════════════════════╝
652
+
653
+ ⚡ Command Ran : \x1b[33mcyrus\x1b[0m
654
+ 🌐 Studio Canvas : \x1b[32m${localUrl}\x1b[0m
655
+ 🛠️ Detected Stack: \x1b[35m${framework.toUpperCase()}\x1b[0m
656
+
657
+ 📂 Sync Target:
658
+ ${Object.entries(outputFiles)
659
+ .filter(([fw]) => !outPath || fw === framework)
660
+ .map(([fw, f]) => ` • [${fw.toUpperCase().padEnd(7)}] -> \x1b[33m${path.relative(process.cwd(), f)}\x1b[0m`)
661
+ .join('\n')}
662
+
663
+ ✨ Editing on the visual canvas automatically updates code in your project!
664
+ 💡 To exit: Press Ctrl+C
665
+ `);
666
+
667
+ if (!noOpen) {
668
+ const platform = process.platform;
669
+ const cmd =
670
+ platform === 'win32' ? `start "" "${embedUrl}"` :
671
+ platform === 'darwin' ? `open "${embedUrl}"` :
672
+ `xdg-open "${embedUrl}"`;
673
+ exec(cmd, (err) => {
674
+ if (err) console.log(` Open in browser manually: ${embedUrl}`);
675
+ });
676
+ }
677
+ });
678
+
679
+ httpServer.once('error', (err) => {
680
+ if (err.code === 'EADDRINUSE' && attemptsLeft > 0) {
681
+ tryListen(currentPort + 1, attemptsLeft - 1);
682
+ } else {
683
+ console.error('\n ✗ Server error:', err.message, '\n');
684
+ process.exit(1);
685
+ }
686
+ });
687
+ }
688
+
689
+ tryListen(port);
690
+ }
691
+
692
+ startServer();