@jetstart/core 1.1.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 (116) hide show
  1. package/.eslintrc.json +6 -0
  2. package/README.md +124 -0
  3. package/dist/build/builder.d.ts +57 -0
  4. package/dist/build/builder.d.ts.map +1 -0
  5. package/dist/build/builder.js +151 -0
  6. package/dist/build/builder.js.map +1 -0
  7. package/dist/build/cache.d.ts +51 -0
  8. package/dist/build/cache.d.ts.map +1 -0
  9. package/dist/build/cache.js +152 -0
  10. package/dist/build/cache.js.map +1 -0
  11. package/dist/build/dsl-parser.d.ts +54 -0
  12. package/dist/build/dsl-parser.d.ts.map +1 -0
  13. package/dist/build/dsl-parser.js +373 -0
  14. package/dist/build/dsl-parser.js.map +1 -0
  15. package/dist/build/dsl-types.d.ts +47 -0
  16. package/dist/build/dsl-types.d.ts.map +1 -0
  17. package/dist/build/dsl-types.js +7 -0
  18. package/dist/build/dsl-types.js.map +1 -0
  19. package/dist/build/gradle-injector.d.ts +14 -0
  20. package/dist/build/gradle-injector.d.ts.map +1 -0
  21. package/dist/build/gradle-injector.js +77 -0
  22. package/dist/build/gradle-injector.js.map +1 -0
  23. package/dist/build/gradle.d.ts +43 -0
  24. package/dist/build/gradle.d.ts.map +1 -0
  25. package/dist/build/gradle.js +281 -0
  26. package/dist/build/gradle.js.map +1 -0
  27. package/dist/build/index.d.ts +10 -0
  28. package/dist/build/index.d.ts.map +1 -0
  29. package/dist/build/index.js +26 -0
  30. package/dist/build/index.js.map +1 -0
  31. package/dist/build/parser.d.ts +12 -0
  32. package/dist/build/parser.d.ts.map +1 -0
  33. package/dist/build/parser.js +71 -0
  34. package/dist/build/parser.js.map +1 -0
  35. package/dist/build/watcher.d.ts +30 -0
  36. package/dist/build/watcher.d.ts.map +1 -0
  37. package/dist/build/watcher.js +120 -0
  38. package/dist/build/watcher.js.map +1 -0
  39. package/dist/index.d.ts +11 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +26 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/server/http.d.ts +12 -0
  44. package/dist/server/http.d.ts.map +1 -0
  45. package/dist/server/http.js +32 -0
  46. package/dist/server/http.js.map +1 -0
  47. package/dist/server/index.d.ts +35 -0
  48. package/dist/server/index.d.ts.map +1 -0
  49. package/dist/server/index.js +262 -0
  50. package/dist/server/index.js.map +1 -0
  51. package/dist/server/middleware.d.ts +7 -0
  52. package/dist/server/middleware.d.ts.map +1 -0
  53. package/dist/server/middleware.js +42 -0
  54. package/dist/server/middleware.js.map +1 -0
  55. package/dist/server/routes.d.ts +7 -0
  56. package/dist/server/routes.d.ts.map +1 -0
  57. package/dist/server/routes.js +104 -0
  58. package/dist/server/routes.js.map +1 -0
  59. package/dist/types/index.d.ts +20 -0
  60. package/dist/types/index.d.ts.map +1 -0
  61. package/dist/types/index.js +6 -0
  62. package/dist/types/index.js.map +1 -0
  63. package/dist/utils/index.d.ts +7 -0
  64. package/dist/utils/index.d.ts.map +1 -0
  65. package/dist/utils/index.js +23 -0
  66. package/dist/utils/index.js.map +1 -0
  67. package/dist/utils/logger.d.ts +10 -0
  68. package/dist/utils/logger.d.ts.map +1 -0
  69. package/dist/utils/logger.js +33 -0
  70. package/dist/utils/logger.js.map +1 -0
  71. package/dist/utils/qr.d.ts +8 -0
  72. package/dist/utils/qr.d.ts.map +1 -0
  73. package/dist/utils/qr.js +48 -0
  74. package/dist/utils/qr.js.map +1 -0
  75. package/dist/utils/session.d.ts +18 -0
  76. package/dist/utils/session.d.ts.map +1 -0
  77. package/dist/utils/session.js +49 -0
  78. package/dist/utils/session.js.map +1 -0
  79. package/dist/websocket/handler.d.ts +25 -0
  80. package/dist/websocket/handler.d.ts.map +1 -0
  81. package/dist/websocket/handler.js +126 -0
  82. package/dist/websocket/handler.js.map +1 -0
  83. package/dist/websocket/index.d.ts +18 -0
  84. package/dist/websocket/index.d.ts.map +1 -0
  85. package/dist/websocket/index.js +40 -0
  86. package/dist/websocket/index.js.map +1 -0
  87. package/dist/websocket/manager.d.ts +16 -0
  88. package/dist/websocket/manager.d.ts.map +1 -0
  89. package/dist/websocket/manager.js +58 -0
  90. package/dist/websocket/manager.js.map +1 -0
  91. package/package.json +78 -0
  92. package/src/build/builder.ts +192 -0
  93. package/src/build/cache.ts +144 -0
  94. package/src/build/dsl-parser.ts +382 -0
  95. package/src/build/dsl-types.ts +50 -0
  96. package/src/build/gradle-injector.ts +64 -0
  97. package/src/build/gradle.ts +305 -0
  98. package/src/build/index.ts +10 -0
  99. package/src/build/parser.ts +75 -0
  100. package/src/build/watcher.ts +103 -0
  101. package/src/index.ts +20 -0
  102. package/src/server/http.ts +38 -0
  103. package/src/server/index.ts +272 -0
  104. package/src/server/middleware.ts +43 -0
  105. package/src/server/routes.ts +116 -0
  106. package/src/types/index.ts +21 -0
  107. package/src/utils/index.ts +7 -0
  108. package/src/utils/logger.ts +28 -0
  109. package/src/utils/qr.ts +46 -0
  110. package/src/utils/session.ts +58 -0
  111. package/src/websocket/handler.ts +150 -0
  112. package/src/websocket/index.ts +56 -0
  113. package/src/websocket/manager.ts +63 -0
  114. package/tests/build.test.ts +13 -0
  115. package/tests/server.test.ts +13 -0
  116. package/tsconfig.json +25 -0
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Main Server Entry Point
3
+ * Starts HTTP and WebSocket servers
4
+ */
5
+
6
+ import * as path from 'path';
7
+ import * as os from 'os';
8
+ import { createHttpServer } from './http';
9
+ import { createWebSocketServer } from '../websocket';
10
+ import { WebSocketHandler } from '../websocket/handler';
11
+ import { log, success, error } from '../utils/logger';
12
+ import { DEFAULT_CORE_PORT, DEFAULT_WS_PORT } from '@jetstart/shared';
13
+ import { SessionManager } from '../utils/session';
14
+ import { BuildService } from '../build';
15
+ import { ServerSession } from '../types';
16
+ import { DSLParser } from '../build/dsl-parser';
17
+ import { injectBuildConfigFields } from '../build/gradle-injector';
18
+
19
+ export interface ServerConfig {
20
+ httpPort?: number;
21
+ wsPort?: number;
22
+ host?: string;
23
+ displayHost?: string; // IP address to display in logs/QR codes (for client connections)
24
+ projectPath?: string;
25
+ projectName?: string;
26
+ }
27
+
28
+ export class JetStartServer {
29
+ private httpServer: any;
30
+ private wsServer: any;
31
+ private wsHandler: WebSocketHandler | null = null;
32
+ private config: Required<ServerConfig> & { displayHost: string };
33
+ private sessionManager: SessionManager;
34
+ private buildService: BuildService;
35
+ private currentSession: ServerSession | null = null;
36
+ private buildMutex: boolean = false; // Prevent concurrent builds
37
+ private latestApkPath: string | null = null; // Store latest built APK path
38
+
39
+ constructor(config: ServerConfig = {}) {
40
+ const bindHost = config.host || '0.0.0.0';
41
+ const displayHost = config.displayHost || bindHost;
42
+
43
+ this.config = {
44
+ httpPort: config.httpPort || DEFAULT_CORE_PORT,
45
+ wsPort: config.wsPort || DEFAULT_WS_PORT,
46
+ host: bindHost,
47
+ displayHost: displayHost,
48
+ projectPath: config.projectPath || process.cwd(),
49
+ projectName: config.projectName || path.basename(config.projectPath || process.cwd()),
50
+ };
51
+
52
+ this.sessionManager = new SessionManager();
53
+ this.buildService = new BuildService({
54
+ cacheEnabled: true,
55
+ cachePath: path.join(os.tmpdir(), 'jetstart-cache'),
56
+ watchEnabled: true,
57
+ });
58
+ }
59
+
60
+ async start(): Promise<ServerSession> {
61
+ try {
62
+ log('Starting JetStart Core server...');
63
+
64
+ // Create development session
65
+ this.currentSession = await this.sessionManager.createSession({
66
+ projectName: this.config.projectName,
67
+ projectPath: this.config.projectPath,
68
+ });
69
+
70
+ // Inject server URL into build.gradle for hot reload
71
+ const serverUrl = `ws://${this.config.displayHost}:${this.config.wsPort}`;
72
+ await injectBuildConfigFields(this.config.projectPath, [
73
+ { type: 'String', name: 'JETSTART_SERVER_URL', value: serverUrl },
74
+ { type: 'String', name: 'JETSTART_SESSION_ID', value: this.currentSession.id }
75
+ ]);
76
+ log(`Injected server URL: ${serverUrl}`);
77
+
78
+ // Start HTTP server
79
+ this.httpServer = await createHttpServer({
80
+ port: this.config.httpPort,
81
+ host: this.config.host,
82
+ getLatestApk: () => this.latestApkPath,
83
+ });
84
+
85
+ // Start WebSocket server
86
+ const wsResult = await createWebSocketServer({
87
+ port: this.config.wsPort,
88
+ onClientConnected: async (sessionId: string) => {
89
+ // Trigger initial build when client connects
90
+ log(`Triggering initial build for connected client (session: ${sessionId})`);
91
+ await this.handleRebuild();
92
+ },
93
+ });
94
+ this.wsServer = wsResult.server;
95
+ this.wsHandler = wsResult.handler;
96
+
97
+ // Setup build service event listeners
98
+ this.setupBuildListeners();
99
+
100
+ // Start watching for file changes
101
+ this.buildService.startWatching(this.config.projectPath, async (files) => {
102
+ log(`Files changed: ${files.map(f => path.basename(f)).join(', ')}`);
103
+
104
+ // Check if only UI files changed (MainActivity.kt, screens/, components/)
105
+ const uiFiles = files.filter(f =>
106
+ f.includes('MainActivity.kt') ||
107
+ f.includes('/screens/') ||
108
+ f.includes('\\screens\\') ||
109
+ f.includes('/components/') ||
110
+ f.includes('\\components\\')
111
+ );
112
+
113
+ // If ALL changed files are UI files, use DSL hot reload (FAST)
114
+ if (uiFiles.length > 0 && uiFiles.length === files.length) {
115
+ log('🚀 UI-only changes detected, using DSL hot reload');
116
+ this.handleUIUpdate(uiFiles[0]);
117
+ } else {
118
+ // Otherwise, full Gradle build
119
+ log('📦 Non-UI changes detected, triggering full Gradle build');
120
+ this.buildService.clearCache();
121
+ await this.handleRebuild();
122
+ }
123
+ });
124
+
125
+ console.log();
126
+ success('JetStart Core is running!');
127
+ log(`HTTP Server: http://${this.config.displayHost}:${this.config.httpPort}`);
128
+ log(`WebSocket Server: ws://${this.config.displayHost}:${this.config.wsPort}`);
129
+ log(`Session ID: ${this.currentSession.id}`);
130
+ log(`Session Token: ${this.currentSession.token}`);
131
+ console.log();
132
+
133
+ return this.currentSession;
134
+
135
+ } catch (err: any) {
136
+ error(`Failed to start server: ${err.message}`);
137
+ throw err;
138
+ }
139
+ }
140
+
141
+ async stop(): Promise<void> {
142
+ log('Stopping JetStart Core server...');
143
+
144
+ // Stop file watching
145
+ this.buildService.stopWatching();
146
+
147
+ if (this.wsServer) {
148
+ await this.wsServer.close();
149
+ }
150
+
151
+ if (this.httpServer) {
152
+ await new Promise<void>((resolve) => {
153
+ this.httpServer.close(() => resolve());
154
+ });
155
+ }
156
+
157
+ success('Server stopped');
158
+ }
159
+
160
+ getSession(): ServerSession | null {
161
+ return this.currentSession;
162
+ }
163
+
164
+ private setupBuildListeners(): void {
165
+ this.buildService.on('build:start', () => {
166
+ log('Build started');
167
+ if (this.wsHandler && this.currentSession) {
168
+ this.wsHandler.sendBuildStart(this.currentSession.id);
169
+ }
170
+ });
171
+
172
+ this.buildService.on('build:complete', (result) => {
173
+ success(`Build completed in ${result.buildTime}ms`);
174
+ if (this.wsHandler && this.currentSession) {
175
+ // Store the APK path
176
+ this.latestApkPath = result.apkPath || null;
177
+
178
+ // Send full download URL to client (not just relative path)
179
+ const downloadUrl = `http://${this.config.displayHost}:${this.config.httpPort}/download/app.apk`;
180
+ this.wsHandler.sendBuildComplete(this.currentSession.id, downloadUrl);
181
+ log(`APK download URL: ${downloadUrl}`);
182
+ }
183
+ });
184
+
185
+ this.buildService.on('build:error', (errorMsg, details) => {
186
+ error(`Build failed: ${errorMsg}`);
187
+ // TODO: Send build-error message via WebSocket
188
+ });
189
+
190
+ this.buildService.on('watch:change', (files) => {
191
+ // This event is just for logging/monitoring
192
+ // Actual build logic is handled in startWatching callback
193
+ });
194
+ }
195
+
196
+ private async handleRebuild(): Promise<void> {
197
+ if (!this.currentSession) {
198
+ return;
199
+ }
200
+
201
+ // Atomic mutex check - prevents race condition
202
+ if (this.buildMutex) {
203
+ log('Build already in progress, skipping duplicate build request');
204
+ return;
205
+ }
206
+
207
+ // Set mutex immediately before any async operations
208
+ this.buildMutex = true;
209
+
210
+ try {
211
+ // Trigger actual build using BuildService
212
+ const result = await this.buildService.build({
213
+ projectPath: this.config.projectPath,
214
+ outputPath: path.join(this.config.projectPath, 'build/outputs/apk'),
215
+ buildType: 'debug' as any, // BuildType.DEBUG
216
+ minifyEnabled: false,
217
+ debuggable: true,
218
+ versionCode: 1,
219
+ versionName: '1.0.0',
220
+ applicationId: 'com.example.app',
221
+ });
222
+
223
+ if (result.success) {
224
+ success(`Build completed successfully: ${result.apkPath || 'APK path not found'}`);
225
+ } else {
226
+ error(`Build failed: ${result.errors?.[0]?.message || 'Unknown error'}`);
227
+ }
228
+ } catch (err: any) {
229
+ error(`Build failed: ${err.message}`);
230
+ } finally {
231
+ // Always release mutex
232
+ this.buildMutex = false;
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Handle UI file updates using DSL hot reload (FAST)
238
+ */
239
+ private handleUIUpdate(filePath: string): void {
240
+ if (!this.currentSession || !this.wsHandler) {
241
+ return;
242
+ }
243
+
244
+ try {
245
+ log(`Parsing UI file: ${path.basename(filePath)}`);
246
+ const parseResult = DSLParser.parseFile(filePath);
247
+
248
+ if (parseResult.success && parseResult.dsl) {
249
+ const dslContent = JSON.stringify(parseResult.dsl);
250
+ log(`DSL generated: ${dslContent.length} bytes`);
251
+
252
+ // Send DSL update via WebSocket (instant hot reload!)
253
+ this.wsHandler.sendUIUpdate(
254
+ this.currentSession.id,
255
+ dslContent,
256
+ [path.basename(filePath)]
257
+ );
258
+
259
+ success(`UI hot reload sent in <100ms ⚡`);
260
+ } else {
261
+ error(`Failed to parse UI file: ${parseResult.errors?.join(', ')}`);
262
+ // Fallback to full build
263
+ log('Falling back to full Gradle build...');
264
+ this.handleRebuild();
265
+ }
266
+ } catch (err: any) {
267
+ error(`UI update failed: ${err.message}`);
268
+ // Fallback to full build
269
+ this.handleRebuild();
270
+ }
271
+ }
272
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Express Middleware
3
+ * CORS, body parsing, error handling
4
+ */
5
+
6
+ import { Express, Request, Response, NextFunction } from 'express';
7
+ import express from 'express';
8
+ import cors from 'cors';
9
+ import { error as logError } from '../utils/logger';
10
+
11
+ export function setupMiddleware(app: Express): void {
12
+ // CORS
13
+ app.use(cors({
14
+ origin: '*',
15
+ methods: ['GET', 'POST', 'PUT', 'DELETE'],
16
+ allowedHeaders: ['Content-Type', 'Authorization'],
17
+ }));
18
+
19
+ // Body parsing
20
+ app.use(express.json());
21
+ app.use(express.urlencoded({ extended: true }));
22
+
23
+ // Request logging
24
+ app.use((req: Request, res: Response, next: NextFunction) => {
25
+ const start = Date.now();
26
+
27
+ res.on('finish', () => {
28
+ const duration = Date.now() - start;
29
+ console.log(`${req.method} ${req.path} - ${res.statusCode} (${duration}ms)`);
30
+ });
31
+
32
+ next();
33
+ });
34
+
35
+ // Error handling
36
+ app.use((err: Error, req: Request, res: Response, _next: NextFunction) => {
37
+ logError(`Server error: ${err.message}`);
38
+ res.status(500).json({
39
+ error: 'Internal server error',
40
+ message: err.message,
41
+ });
42
+ });
43
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * HTTP Routes
3
+ * REST API endpoints
4
+ */
5
+
6
+ import { Express, Request, Response } from 'express';
7
+ import path from 'path';
8
+ import fs from 'fs';
9
+ import { SessionManager } from '../utils/session';
10
+ import { generateQRCode } from '../utils/qr';
11
+ import { JETSTART_VERSION } from '@jetstart/shared';
12
+
13
+ const sessionManager = new SessionManager();
14
+
15
+ export function setupRoutes(app: Express, getLatestApk?: () => string | null): void {
16
+ // Health check
17
+ app.get('/health', (req: Request, res: Response) => {
18
+ res.json({
19
+ status: 'ok',
20
+ version: JETSTART_VERSION,
21
+ uptime: process.uptime(),
22
+ });
23
+ });
24
+
25
+ // Get version
26
+ app.get('/version', (req: Request, res: Response) => {
27
+ res.json({
28
+ version: JETSTART_VERSION,
29
+ });
30
+ });
31
+
32
+ // Create new session
33
+ app.post('/session/create', async (req: Request, res: Response) => {
34
+ try {
35
+ const { projectName, projectPath } = req.body;
36
+
37
+ if (!projectName || !projectPath) {
38
+ return res.status(400).json({
39
+ error: 'Missing required fields: projectName, projectPath',
40
+ });
41
+ }
42
+
43
+ const session = await sessionManager.createSession({
44
+ projectName,
45
+ projectPath,
46
+ });
47
+
48
+ // Generate QR code
49
+ const qrCode = await generateQRCode({
50
+ sessionId: session.id,
51
+ serverUrl: `http://${req.hostname}:${req.socket.localPort}`,
52
+ wsUrl: `ws://${req.hostname}:${req.socket.localPort}`,
53
+ token: session.token,
54
+ projectName,
55
+ version: JETSTART_VERSION,
56
+ });
57
+
58
+ res.json({
59
+ session,
60
+ qrCode,
61
+ });
62
+
63
+ } catch (err: any) {
64
+ res.status(500).json({
65
+ error: 'Failed to create session',
66
+ message: err.message,
67
+ });
68
+ }
69
+ });
70
+
71
+ // Get session
72
+ app.get('/session/:sessionId', (req: Request, res: Response) => {
73
+ const session = sessionManager.getSession(req.params.sessionId);
74
+
75
+ if (!session) {
76
+ return res.status(404).json({
77
+ error: 'Session not found',
78
+ });
79
+ }
80
+
81
+ res.json(session);
82
+ });
83
+
84
+ // Download APK
85
+ app.get('/download/:filename', async (req: Request, res: Response) => {
86
+ // Get latest built APK path
87
+ const apkPath = getLatestApk?.();
88
+
89
+ if (!apkPath) {
90
+ return res.status(404).json({ error: 'No APK available. Build the app first.' });
91
+ }
92
+
93
+ // Check if file exists
94
+ if (!fs.existsSync(apkPath)) {
95
+ return res.status(404).json({ error: 'APK file not found at expected location' });
96
+ }
97
+
98
+ // Send the APK file
99
+ res.download(apkPath, req.params.filename || 'app-debug.apk', (err) => {
100
+ if (err) {
101
+ console.error(`Failed to send APK: ${err.message}`);
102
+ if (!res.headersSent) {
103
+ res.status(500).json({ error: 'Failed to send APK file' });
104
+ }
105
+ }
106
+ });
107
+ });
108
+
109
+ // 404 handler
110
+ app.use((req: Request, res: Response) => {
111
+ res.status(404).json({
112
+ error: 'Not found',
113
+ path: req.path,
114
+ });
115
+ });
116
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Core-specific types
3
+ */
4
+
5
+ export interface ServerSession {
6
+ id: string;
7
+ token: string;
8
+ projectName: string;
9
+ projectPath: string;
10
+ createdAt: number;
11
+ lastActivity: number;
12
+ }
13
+
14
+ export interface QRCodeOptions {
15
+ sessionId: string;
16
+ serverUrl: string;
17
+ wsUrl: string;
18
+ token: string;
19
+ projectName: string;
20
+ version: string;
21
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Export all utilities
3
+ */
4
+
5
+ export * from './logger';
6
+ export * from './qr';
7
+ export * from './session';
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Logger Utility
3
+ * Colored logging for Core
4
+ */
5
+
6
+ import chalk from 'chalk';
7
+
8
+ export function log(message: string) {
9
+ console.log(chalk.cyan('[Core]'), message);
10
+ }
11
+
12
+ export function success(message: string) {
13
+ console.log(chalk.green('✔'), chalk.cyan('[Core]'), message);
14
+ }
15
+
16
+ export function error(message: string) {
17
+ console.error(chalk.red('✖'), chalk.cyan('[Core]'), message);
18
+ }
19
+
20
+ export function warn(message: string) {
21
+ console.log(chalk.yellow('⚠'), chalk.cyan('[Core]'), message);
22
+ }
23
+
24
+ export function debug(message: string) {
25
+ if (process.env.DEBUG) {
26
+ console.log(chalk.gray('[DEBUG]'), chalk.cyan('[Core]'), message);
27
+ }
28
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * QR Code Generator
3
+ * Generates QR codes for device pairing
4
+ */
5
+
6
+ import QRCode from 'qrcode';
7
+ import { QRCodeData } from '@jetstart/shared';
8
+ import { QRCodeOptions } from '../types';
9
+
10
+ export async function generateQRCode(options: QRCodeOptions): Promise<string> {
11
+ const data: QRCodeData = {
12
+ sessionId: options.sessionId,
13
+ serverUrl: options.serverUrl,
14
+ wsUrl: options.wsUrl,
15
+ token: options.token,
16
+ projectName: options.projectName,
17
+ version: options.version,
18
+ };
19
+
20
+ // Generate QR code as data URL
21
+ return QRCode.toDataURL(JSON.stringify(data), {
22
+ errorCorrectionLevel: 'M',
23
+ type: 'image/png',
24
+ width: 300,
25
+ });
26
+ }
27
+
28
+ export async function generateQRCodeTerminal(options: QRCodeOptions): Promise<void> {
29
+ const data: QRCodeData = {
30
+ sessionId: options.sessionId,
31
+ serverUrl: options.serverUrl,
32
+ wsUrl: options.wsUrl,
33
+ token: options.token,
34
+ projectName: options.projectName,
35
+ version: options.version,
36
+ };
37
+
38
+ // Generate QR code for terminal
39
+ return QRCode.toString(JSON.stringify(data), {
40
+ type: 'terminal',
41
+ small: true,
42
+ }, (err, url) => {
43
+ if (err) throw err;
44
+ console.log(url);
45
+ });
46
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Session Manager
3
+ * Manages development sessions
4
+ */
5
+
6
+ import { v4 as uuidv4 } from 'uuid';
7
+ import { ServerSession } from '../types';
8
+ import { SESSION_TOKEN_EXPIRY } from '@jetstart/shared';
9
+
10
+ export class SessionManager {
11
+ private sessions: Map<string, ServerSession> = new Map();
12
+
13
+ async createSession(data: {
14
+ projectName: string;
15
+ projectPath: string;
16
+ }): Promise<ServerSession> {
17
+ const session: ServerSession = {
18
+ id: uuidv4(),
19
+ token: this.generateToken(),
20
+ projectName: data.projectName,
21
+ projectPath: data.projectPath,
22
+ createdAt: Date.now(),
23
+ lastActivity: Date.now(),
24
+ };
25
+
26
+ this.sessions.set(session.id, session);
27
+ return session;
28
+ }
29
+
30
+ getSession(sessionId: string): ServerSession | undefined {
31
+ return this.sessions.get(sessionId);
32
+ }
33
+
34
+ updateActivity(sessionId: string): void {
35
+ const session = this.sessions.get(sessionId);
36
+ if (session) {
37
+ session.lastActivity = Date.now();
38
+ }
39
+ }
40
+
41
+ deleteSession(sessionId: string): void {
42
+ this.sessions.delete(sessionId);
43
+ }
44
+
45
+ cleanupExpiredSessions(): void {
46
+ const now = Date.now();
47
+
48
+ for (const [id, session] of this.sessions.entries()) {
49
+ if (now - session.lastActivity > SESSION_TOKEN_EXPIRY) {
50
+ this.sessions.delete(id);
51
+ }
52
+ }
53
+ }
54
+
55
+ private generateToken(): string {
56
+ return uuidv4().replace(/-/g, '');
57
+ }
58
+ }