@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.
- package/.eslintrc.json +6 -0
- package/README.md +124 -0
- package/dist/build/builder.d.ts +57 -0
- package/dist/build/builder.d.ts.map +1 -0
- package/dist/build/builder.js +151 -0
- package/dist/build/builder.js.map +1 -0
- package/dist/build/cache.d.ts +51 -0
- package/dist/build/cache.d.ts.map +1 -0
- package/dist/build/cache.js +152 -0
- package/dist/build/cache.js.map +1 -0
- package/dist/build/dsl-parser.d.ts +54 -0
- package/dist/build/dsl-parser.d.ts.map +1 -0
- package/dist/build/dsl-parser.js +373 -0
- package/dist/build/dsl-parser.js.map +1 -0
- package/dist/build/dsl-types.d.ts +47 -0
- package/dist/build/dsl-types.d.ts.map +1 -0
- package/dist/build/dsl-types.js +7 -0
- package/dist/build/dsl-types.js.map +1 -0
- package/dist/build/gradle-injector.d.ts +14 -0
- package/dist/build/gradle-injector.d.ts.map +1 -0
- package/dist/build/gradle-injector.js +77 -0
- package/dist/build/gradle-injector.js.map +1 -0
- package/dist/build/gradle.d.ts +43 -0
- package/dist/build/gradle.d.ts.map +1 -0
- package/dist/build/gradle.js +281 -0
- package/dist/build/gradle.js.map +1 -0
- package/dist/build/index.d.ts +10 -0
- package/dist/build/index.d.ts.map +1 -0
- package/dist/build/index.js +26 -0
- package/dist/build/index.js.map +1 -0
- package/dist/build/parser.d.ts +12 -0
- package/dist/build/parser.d.ts.map +1 -0
- package/dist/build/parser.js +71 -0
- package/dist/build/parser.js.map +1 -0
- package/dist/build/watcher.d.ts +30 -0
- package/dist/build/watcher.d.ts.map +1 -0
- package/dist/build/watcher.js +120 -0
- package/dist/build/watcher.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/server/http.d.ts +12 -0
- package/dist/server/http.d.ts.map +1 -0
- package/dist/server/http.js +32 -0
- package/dist/server/http.js.map +1 -0
- package/dist/server/index.d.ts +35 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +262 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/middleware.d.ts +7 -0
- package/dist/server/middleware.d.ts.map +1 -0
- package/dist/server/middleware.js +42 -0
- package/dist/server/middleware.js.map +1 -0
- package/dist/server/routes.d.ts +7 -0
- package/dist/server/routes.d.ts.map +1 -0
- package/dist/server/routes.js +104 -0
- package/dist/server/routes.js.map +1 -0
- package/dist/types/index.d.ts +20 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +6 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/index.d.ts +7 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +23 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logger.d.ts +10 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +33 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/qr.d.ts +8 -0
- package/dist/utils/qr.d.ts.map +1 -0
- package/dist/utils/qr.js +48 -0
- package/dist/utils/qr.js.map +1 -0
- package/dist/utils/session.d.ts +18 -0
- package/dist/utils/session.d.ts.map +1 -0
- package/dist/utils/session.js +49 -0
- package/dist/utils/session.js.map +1 -0
- package/dist/websocket/handler.d.ts +25 -0
- package/dist/websocket/handler.d.ts.map +1 -0
- package/dist/websocket/handler.js +126 -0
- package/dist/websocket/handler.js.map +1 -0
- package/dist/websocket/index.d.ts +18 -0
- package/dist/websocket/index.d.ts.map +1 -0
- package/dist/websocket/index.js +40 -0
- package/dist/websocket/index.js.map +1 -0
- package/dist/websocket/manager.d.ts +16 -0
- package/dist/websocket/manager.d.ts.map +1 -0
- package/dist/websocket/manager.js +58 -0
- package/dist/websocket/manager.js.map +1 -0
- package/package.json +78 -0
- package/src/build/builder.ts +192 -0
- package/src/build/cache.ts +144 -0
- package/src/build/dsl-parser.ts +382 -0
- package/src/build/dsl-types.ts +50 -0
- package/src/build/gradle-injector.ts +64 -0
- package/src/build/gradle.ts +305 -0
- package/src/build/index.ts +10 -0
- package/src/build/parser.ts +75 -0
- package/src/build/watcher.ts +103 -0
- package/src/index.ts +20 -0
- package/src/server/http.ts +38 -0
- package/src/server/index.ts +272 -0
- package/src/server/middleware.ts +43 -0
- package/src/server/routes.ts +116 -0
- package/src/types/index.ts +21 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/logger.ts +28 -0
- package/src/utils/qr.ts +46 -0
- package/src/utils/session.ts +58 -0
- package/src/websocket/handler.ts +150 -0
- package/src/websocket/index.ts +56 -0
- package/src/websocket/manager.ts +63 -0
- package/tests/build.test.ts +13 -0
- package/tests/server.test.ts +13 -0
- 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,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
|
+
}
|
package/src/utils/qr.ts
ADDED
|
@@ -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
|
+
}
|