@exreve/exk 1.0.7 → 1.0.8
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/bin/exk +5 -34
- package/dist/agentLogger.js +143 -0
- package/dist/agentSession.js +1020 -0
- package/dist/app-child.js +2589 -0
- package/dist/appManager.js +222 -0
- package/dist/appRunner.js +383 -0
- package/dist/index.js +2745 -0
- package/dist/moduleMcpServer.js +93 -0
- package/dist/projectAnalyzer.js +323 -0
- package/dist/projectManager.js +92 -0
- package/{runnerGenerator.ts → dist/runnerGenerator.js} +29 -36
- package/dist/shared/types.js +2 -0
- package/dist/skills/index.js +117 -0
- package/dist/ttc-cli.tar.gz +0 -0
- package/dist/updater.js +425 -0
- package/package.json +6 -8
- package/agentLogger.ts +0 -162
- package/agentSession.ts +0 -1176
- package/app-child.ts +0 -2872
- package/appManager.ts +0 -275
- package/appRunner.ts +0 -475
- package/index.ts +0 -3049
- package/moduleMcpServer.ts +0 -131
- package/projectAnalyzer.ts +0 -341
- package/projectManager.ts +0 -111
- package/shared/types.ts +0 -488
- package/tsconfig.json +0 -22
- package/updater.ts +0 -512
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { createRunner } from './appRunner.js';
|
|
5
|
+
// Store running app runners: projectId -> appName -> runner
|
|
6
|
+
const runningApps = new Map();
|
|
7
|
+
const LOGS_DIR = path.join(os.homedir(), '.talk-to-code', 'app-logs');
|
|
8
|
+
/**
|
|
9
|
+
* Ensure logs directory exists
|
|
10
|
+
*/
|
|
11
|
+
const ensureLogsDir = () => fs.mkdir(LOGS_DIR, { recursive: true });
|
|
12
|
+
/**
|
|
13
|
+
* Helper to remove app from running apps and clean up empty project maps
|
|
14
|
+
*/
|
|
15
|
+
const removeAppFromRunning = (projectId, appName) => {
|
|
16
|
+
const projectApps = runningApps.get(projectId);
|
|
17
|
+
if (projectApps) {
|
|
18
|
+
projectApps.delete(appName);
|
|
19
|
+
if (projectApps.size === 0) {
|
|
20
|
+
runningApps.delete(projectId);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Get log file path for an app
|
|
26
|
+
*/
|
|
27
|
+
function getLogFilePath(projectId, appName) {
|
|
28
|
+
return path.join(LOGS_DIR, `${projectId}-${appName}.log`);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Start an app using wrapper runner
|
|
32
|
+
*/
|
|
33
|
+
export async function startApp(projectPath, projectId, app) {
|
|
34
|
+
try {
|
|
35
|
+
// Check if already running
|
|
36
|
+
if (isAppRunning(projectId, app.name)) {
|
|
37
|
+
const running = getRunningApp(projectId, app.name);
|
|
38
|
+
return {
|
|
39
|
+
success: true,
|
|
40
|
+
processId: `${projectId}-${app.name}`,
|
|
41
|
+
pid: running?.pid
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
// Ensure logs directory exists
|
|
45
|
+
await ensureLogsDir();
|
|
46
|
+
const logFile = getLogFilePath(projectId, app.name);
|
|
47
|
+
// Create runner based on app type
|
|
48
|
+
const runner = createRunner(app, projectPath, projectId, {
|
|
49
|
+
onOutput: async (output) => {
|
|
50
|
+
// Logs are handled by the runner itself
|
|
51
|
+
},
|
|
52
|
+
onError: (error) => {
|
|
53
|
+
// Errors are logged by runner
|
|
54
|
+
},
|
|
55
|
+
onExit: (code) => {
|
|
56
|
+
removeAppFromRunning(projectId, app.name);
|
|
57
|
+
},
|
|
58
|
+
onStats: (stats) => {
|
|
59
|
+
// Stats updates can be used for monitoring
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
// Start the runner
|
|
63
|
+
const result = await runner.start();
|
|
64
|
+
if (!result.success) {
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
// Store runner info
|
|
68
|
+
if (!runningApps.has(projectId)) {
|
|
69
|
+
runningApps.set(projectId, new Map());
|
|
70
|
+
}
|
|
71
|
+
runningApps.get(projectId).set(app.name, {
|
|
72
|
+
runner,
|
|
73
|
+
pid: result.pid,
|
|
74
|
+
startTime: Date.now(),
|
|
75
|
+
logFile,
|
|
76
|
+
});
|
|
77
|
+
return {
|
|
78
|
+
success: true,
|
|
79
|
+
processId: `${projectId}-${app.name}`,
|
|
80
|
+
pid: result.pid,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
return {
|
|
85
|
+
success: false,
|
|
86
|
+
error: error.message || 'Failed to start app'
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Stop an app using wrapper runner
|
|
92
|
+
*/
|
|
93
|
+
export async function stopApp(projectId, appName, app) {
|
|
94
|
+
try {
|
|
95
|
+
const running = getRunningApp(projectId, appName);
|
|
96
|
+
if (!running) {
|
|
97
|
+
return {
|
|
98
|
+
success: false,
|
|
99
|
+
error: 'App is not running'
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// Stop using runner
|
|
103
|
+
const result = await running.runner.stop();
|
|
104
|
+
if (result.success) {
|
|
105
|
+
removeAppFromRunning(projectId, appName);
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
return {
|
|
111
|
+
success: false,
|
|
112
|
+
error: error.message || 'Failed to stop app'
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Restart an app
|
|
118
|
+
*/
|
|
119
|
+
export async function restartApp(projectPath, projectId, app) {
|
|
120
|
+
// Stop first if running
|
|
121
|
+
if (isAppRunning(projectId, app.name)) {
|
|
122
|
+
const stopResult = await stopApp(projectId, app.name, app);
|
|
123
|
+
if (!stopResult.success) {
|
|
124
|
+
return stopResult;
|
|
125
|
+
}
|
|
126
|
+
// Wait a bit before restarting
|
|
127
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
128
|
+
}
|
|
129
|
+
// Start
|
|
130
|
+
return startApp(projectPath, projectId, app);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Check if an app is running
|
|
134
|
+
*/
|
|
135
|
+
export function isAppRunning(projectId, appName) {
|
|
136
|
+
const projectApps = runningApps.get(projectId);
|
|
137
|
+
if (!projectApps)
|
|
138
|
+
return false;
|
|
139
|
+
const app = projectApps.get(appName);
|
|
140
|
+
if (!app)
|
|
141
|
+
return false;
|
|
142
|
+
// Check if process is still alive
|
|
143
|
+
try {
|
|
144
|
+
process.kill(app.pid, 0); // Signal 0 just checks if process exists
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// Process doesn't exist, clean up
|
|
149
|
+
removeAppFromRunning(projectId, appName);
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Get running app info
|
|
155
|
+
*/
|
|
156
|
+
export function getRunningApp(projectId, appName) {
|
|
157
|
+
const projectApps = runningApps.get(projectId);
|
|
158
|
+
if (!projectApps)
|
|
159
|
+
return null;
|
|
160
|
+
const app = projectApps.get(appName);
|
|
161
|
+
if (!app)
|
|
162
|
+
return null;
|
|
163
|
+
// Verify process is still alive
|
|
164
|
+
if (!isAppRunning(projectId, appName)) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
return app;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Get status of all apps for a project
|
|
171
|
+
*/
|
|
172
|
+
export function getAppStatuses(projectId, apps) {
|
|
173
|
+
return apps.map(app => {
|
|
174
|
+
const running = getRunningApp(projectId, app.name);
|
|
175
|
+
return {
|
|
176
|
+
name: app.name,
|
|
177
|
+
running: !!running,
|
|
178
|
+
processId: running ? `${projectId}-${app.name}` : undefined,
|
|
179
|
+
pid: running?.pid,
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Get logs for an app
|
|
185
|
+
*/
|
|
186
|
+
export async function getAppLogs(projectId, appName, lines = 100) {
|
|
187
|
+
try {
|
|
188
|
+
const logFile = getLogFilePath(projectId, appName);
|
|
189
|
+
try {
|
|
190
|
+
await fs.access(logFile);
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return {
|
|
194
|
+
success: true,
|
|
195
|
+
logs: 'No logs available yet'
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const content = await fs.readFile(logFile, 'utf-8');
|
|
199
|
+
const logLines = content.split('\n');
|
|
200
|
+
const recentLogs = logLines.slice(-lines).join('\n');
|
|
201
|
+
return {
|
|
202
|
+
success: true,
|
|
203
|
+
logs: recentLogs || 'No logs available'
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
return {
|
|
208
|
+
success: false,
|
|
209
|
+
error: error.message || 'Failed to read logs'
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Get stats for a running app
|
|
215
|
+
*/
|
|
216
|
+
export function getAppStats(projectId, appName) {
|
|
217
|
+
const running = getRunningApp(projectId, appName);
|
|
218
|
+
if (!running) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
return running.runner.getStats();
|
|
222
|
+
}
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
/** Cross-platform: run a shell command (sh -c on Unix, cmd /c on Windows) */
|
|
6
|
+
function shellSpawnOpts(command) {
|
|
7
|
+
if (process.platform === 'win32') {
|
|
8
|
+
return { shell: process.env.COMSPEC || 'cmd.exe', args: ['/c', command] };
|
|
9
|
+
}
|
|
10
|
+
return { shell: 'sh', args: ['-c', command] };
|
|
11
|
+
}
|
|
12
|
+
import Fastify from 'fastify';
|
|
13
|
+
import fastifyStatic from '@fastify/static';
|
|
14
|
+
// ============ Base Runner ============
|
|
15
|
+
export class BaseRunner {
|
|
16
|
+
app;
|
|
17
|
+
projectPath;
|
|
18
|
+
projectId;
|
|
19
|
+
callbacks;
|
|
20
|
+
stats;
|
|
21
|
+
process = null;
|
|
22
|
+
logFile;
|
|
23
|
+
constructor(app, projectPath, projectId, callbacks) {
|
|
24
|
+
this.app = app;
|
|
25
|
+
this.projectPath = projectPath;
|
|
26
|
+
this.projectId = projectId;
|
|
27
|
+
this.callbacks = callbacks;
|
|
28
|
+
this.stats = {
|
|
29
|
+
startTime: Date.now(),
|
|
30
|
+
requests: 0,
|
|
31
|
+
errors: 0,
|
|
32
|
+
};
|
|
33
|
+
this.logFile = path.join(os.homedir(), '.talk-to-code', 'app-logs', `${projectId}-${app.name}.log`);
|
|
34
|
+
}
|
|
35
|
+
async writeLog(type, data) {
|
|
36
|
+
const timestamp = new Date().toISOString();
|
|
37
|
+
const logLine = `[${timestamp}] [${type.toUpperCase()}] ${data}\n`;
|
|
38
|
+
try {
|
|
39
|
+
await fs.appendFile(this.logFile, logLine, 'utf-8');
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
// Ignore log write errors
|
|
43
|
+
}
|
|
44
|
+
this.callbacks.onOutput?.({
|
|
45
|
+
type,
|
|
46
|
+
data,
|
|
47
|
+
timestamp: Date.now(),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
updateStats() {
|
|
51
|
+
if (this.process) {
|
|
52
|
+
try {
|
|
53
|
+
const memUsage = process.memoryUsage();
|
|
54
|
+
this.stats.memoryUsage = memUsage;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Ignore
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
this.callbacks.onStats?.(this.stats);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// ============ Static Frontend Runner ============
|
|
64
|
+
export class StaticFrontendRunner extends BaseRunner {
|
|
65
|
+
fastify = null;
|
|
66
|
+
port;
|
|
67
|
+
constructor(app, projectPath, projectId, callbacks) {
|
|
68
|
+
super(app, projectPath, projectId, callbacks);
|
|
69
|
+
this.port = app.port || 3000;
|
|
70
|
+
}
|
|
71
|
+
async start() {
|
|
72
|
+
try {
|
|
73
|
+
await this.writeLog('system', `Starting static frontend server for ${this.app.name} on port ${this.port}`);
|
|
74
|
+
// Determine build directory (use buildDir from config if provided)
|
|
75
|
+
const buildDirName = this.app.buildDir || 'dist';
|
|
76
|
+
const buildDir = this.app.directory
|
|
77
|
+
? path.join(this.projectPath, this.app.directory, buildDirName)
|
|
78
|
+
: path.join(this.projectPath, buildDirName);
|
|
79
|
+
// Check if specified build dir exists, fallback to dist, build, or public
|
|
80
|
+
let staticDir = buildDir;
|
|
81
|
+
const dirsToTry = buildDirName !== 'dist' ? [buildDirName, 'dist', 'build', 'public'] : ['dist', 'build', 'public'];
|
|
82
|
+
let found = false;
|
|
83
|
+
for (const dirName of dirsToTry) {
|
|
84
|
+
const testDir = this.app.directory
|
|
85
|
+
? path.join(this.projectPath, this.app.directory, dirName)
|
|
86
|
+
: path.join(this.projectPath, dirName);
|
|
87
|
+
try {
|
|
88
|
+
await fs.access(testDir);
|
|
89
|
+
staticDir = testDir;
|
|
90
|
+
found = true;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (!found) {
|
|
98
|
+
return {
|
|
99
|
+
success: false,
|
|
100
|
+
error: `No build directory found. Tried: ${dirsToTry.join(', ')}`
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
await this.writeLog('system', `Serving static files from: ${staticDir}`);
|
|
104
|
+
// Create Fastify instance
|
|
105
|
+
this.fastify = Fastify({
|
|
106
|
+
logger: {
|
|
107
|
+
level: 'info',
|
|
108
|
+
transport: {
|
|
109
|
+
target: 'pino-pretty',
|
|
110
|
+
options: {
|
|
111
|
+
translateTime: 'HH:MM:ss Z',
|
|
112
|
+
ignore: 'pid,hostname',
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
// Register static file serving
|
|
118
|
+
await this.fastify.register(fastifyStatic, {
|
|
119
|
+
root: staticDir,
|
|
120
|
+
prefix: '/',
|
|
121
|
+
});
|
|
122
|
+
// Add request logging middleware
|
|
123
|
+
this.fastify.addHook('onRequest', async (request, reply) => {
|
|
124
|
+
this.stats.requests++;
|
|
125
|
+
this.stats.lastRequestTime = Date.now();
|
|
126
|
+
this.updateStats();
|
|
127
|
+
await this.writeLog('stdout', `${request.method} ${request.url} - ${reply.statusCode}`);
|
|
128
|
+
});
|
|
129
|
+
// Add error handling
|
|
130
|
+
this.fastify.setErrorHandler(async (error, request, reply) => {
|
|
131
|
+
this.stats.errors++;
|
|
132
|
+
this.updateStats();
|
|
133
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
134
|
+
await this.writeLog('stderr', `Error: ${errorMessage} - ${request.method} ${request.url}`);
|
|
135
|
+
reply.status(500).send({ error: 'Internal Server Error' });
|
|
136
|
+
});
|
|
137
|
+
// Start server
|
|
138
|
+
await this.fastify.listen({ port: this.port, host: '0.0.0.0' });
|
|
139
|
+
await this.writeLog('system', `Static server started successfully on port ${this.port}`);
|
|
140
|
+
// Create a mock process for compatibility
|
|
141
|
+
this.process = {
|
|
142
|
+
pid: process.pid,
|
|
143
|
+
kill: (signal) => {
|
|
144
|
+
this.stop();
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
return {
|
|
148
|
+
success: true,
|
|
149
|
+
pid: process.pid,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
await this.writeLog('stderr', `Failed to start static server: ${error.message}`);
|
|
154
|
+
this.callbacks.onError?.(error.message);
|
|
155
|
+
return {
|
|
156
|
+
success: false,
|
|
157
|
+
error: error.message || 'Failed to start static server'
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async stop() {
|
|
162
|
+
try {
|
|
163
|
+
if (this.fastify) {
|
|
164
|
+
await this.writeLog('system', `Stopping static server for ${this.app.name}`);
|
|
165
|
+
await this.fastify.close();
|
|
166
|
+
this.fastify = null;
|
|
167
|
+
await this.writeLog('system', `Static server stopped`);
|
|
168
|
+
this.callbacks.onExit?.(0);
|
|
169
|
+
return { success: true };
|
|
170
|
+
}
|
|
171
|
+
return { success: true };
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
await this.writeLog('stderr', `Error stopping static server: ${error.message}`);
|
|
175
|
+
return {
|
|
176
|
+
success: false,
|
|
177
|
+
error: error.message || 'Failed to stop static server'
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
getStats() {
|
|
182
|
+
return {
|
|
183
|
+
...this.stats,
|
|
184
|
+
memoryUsage: process.memoryUsage(),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// ============ Backend Runner (Express/Fastify wrapper) ============
|
|
189
|
+
export class BackendRunner extends BaseRunner {
|
|
190
|
+
// process is inherited from BaseRunner as protected
|
|
191
|
+
async start() {
|
|
192
|
+
try {
|
|
193
|
+
await this.writeLog('system', `Starting backend app: ${this.app.name}`);
|
|
194
|
+
// Determine working directory
|
|
195
|
+
const workingDir = this.app.directory
|
|
196
|
+
? path.join(this.projectPath, this.app.directory)
|
|
197
|
+
: this.projectPath;
|
|
198
|
+
// Check if directory exists
|
|
199
|
+
try {
|
|
200
|
+
await fs.access(workingDir);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return {
|
|
204
|
+
success: false,
|
|
205
|
+
error: `App directory does not exist: ${this.app.directory || 'root'}`
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
// Prepare environment variables
|
|
209
|
+
const env = {
|
|
210
|
+
...process.env,
|
|
211
|
+
...this.app.env,
|
|
212
|
+
NODE_ENV: this.app.env?.NODE_ENV || process.env.NODE_ENV || 'development',
|
|
213
|
+
PORT: this.app.port?.toString() || process.env.PORT || '3000',
|
|
214
|
+
};
|
|
215
|
+
// Ensure log directory exists
|
|
216
|
+
const logDir = path.dirname(this.logFile);
|
|
217
|
+
await fs.mkdir(logDir, { recursive: true });
|
|
218
|
+
// Spawn the backend process (cross-platform shell)
|
|
219
|
+
const { shell, args } = shellSpawnOpts(this.app.startCommand);
|
|
220
|
+
this.process = spawn(shell, args, {
|
|
221
|
+
cwd: workingDir,
|
|
222
|
+
env,
|
|
223
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
224
|
+
detached: false,
|
|
225
|
+
});
|
|
226
|
+
if (!this.process.pid) {
|
|
227
|
+
return {
|
|
228
|
+
success: false,
|
|
229
|
+
error: 'Failed to spawn process'
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
await this.writeLog('system', `Backend process started with PID: ${this.process.pid}`);
|
|
233
|
+
// Capture stdout
|
|
234
|
+
this.process.stdout?.on('data', async (data) => {
|
|
235
|
+
const output = data.toString();
|
|
236
|
+
await this.writeLog('stdout', output);
|
|
237
|
+
});
|
|
238
|
+
// Capture stderr
|
|
239
|
+
this.process.stderr?.on('data', async (data) => {
|
|
240
|
+
const output = data.toString();
|
|
241
|
+
this.stats.errors++;
|
|
242
|
+
this.updateStats();
|
|
243
|
+
await this.writeLog('stderr', output);
|
|
244
|
+
});
|
|
245
|
+
// Handle process exit
|
|
246
|
+
this.process.on('exit', async (code) => {
|
|
247
|
+
await this.writeLog('system', `Process exited with code ${code}`);
|
|
248
|
+
this.process = null;
|
|
249
|
+
this.callbacks.onExit?.(code);
|
|
250
|
+
});
|
|
251
|
+
// Handle process errors
|
|
252
|
+
this.process.on('error', async (error) => {
|
|
253
|
+
this.stats.errors++;
|
|
254
|
+
this.updateStats();
|
|
255
|
+
await this.writeLog('stderr', `Process error: ${error.message}`);
|
|
256
|
+
this.callbacks.onError?.(error.message);
|
|
257
|
+
});
|
|
258
|
+
// Start stats collection interval
|
|
259
|
+
const statsInterval = setInterval(() => {
|
|
260
|
+
if (this.process) {
|
|
261
|
+
this.updateStats();
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
clearInterval(statsInterval);
|
|
265
|
+
}
|
|
266
|
+
}, 5000);
|
|
267
|
+
return {
|
|
268
|
+
success: true,
|
|
269
|
+
pid: this.process.pid,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
await this.writeLog('stderr', `Failed to start backend: ${error.message}`);
|
|
274
|
+
this.callbacks.onError?.(error.message);
|
|
275
|
+
return {
|
|
276
|
+
success: false,
|
|
277
|
+
error: error.message || 'Failed to start backend'
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async stop() {
|
|
282
|
+
try {
|
|
283
|
+
if (!this.process) {
|
|
284
|
+
return { success: true };
|
|
285
|
+
}
|
|
286
|
+
await this.writeLog('system', `Stopping backend app: ${this.app.name}`);
|
|
287
|
+
// Try custom stop command first
|
|
288
|
+
if (this.app.stopCommand) {
|
|
289
|
+
try {
|
|
290
|
+
const workingDir = this.app.directory
|
|
291
|
+
? path.join(this.projectPath, this.app.directory)
|
|
292
|
+
: this.projectPath;
|
|
293
|
+
const { shell: stopShell, args: stopArgs } = shellSpawnOpts(this.app.stopCommand);
|
|
294
|
+
const stopProcess = spawn(stopShell, stopArgs, {
|
|
295
|
+
cwd: workingDir,
|
|
296
|
+
stdio: 'ignore',
|
|
297
|
+
});
|
|
298
|
+
// Wait for graceful shutdown
|
|
299
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
300
|
+
// If still running, kill it
|
|
301
|
+
if (this.process && this.process.pid) {
|
|
302
|
+
try {
|
|
303
|
+
process.kill(this.process.pid, 0); // Check if still alive
|
|
304
|
+
this.process.kill('SIGTERM');
|
|
305
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
306
|
+
if (this.process && this.process.pid) {
|
|
307
|
+
try {
|
|
308
|
+
process.kill(this.process.pid, 0);
|
|
309
|
+
this.process.kill('SIGKILL');
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
// Process already dead
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
// Process already dead
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
// Fall back to killing process
|
|
323
|
+
if (this.process) {
|
|
324
|
+
this.process.kill('SIGTERM');
|
|
325
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
326
|
+
if (this.process) {
|
|
327
|
+
this.process.kill('SIGKILL');
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
// Default: kill the process
|
|
334
|
+
this.process.kill('SIGTERM');
|
|
335
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
336
|
+
if (this.process) {
|
|
337
|
+
this.process.kill('SIGKILL');
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
this.process = null;
|
|
341
|
+
await this.writeLog('system', `Backend app stopped`);
|
|
342
|
+
return { success: true };
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
await this.writeLog('stderr', `Error stopping backend: ${error.message}`);
|
|
346
|
+
return {
|
|
347
|
+
success: false,
|
|
348
|
+
error: error.message || 'Failed to stop backend'
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
getStats() {
|
|
353
|
+
return {
|
|
354
|
+
...this.stats,
|
|
355
|
+
memoryUsage: this.process ? process.memoryUsage() : undefined,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
// ============ Runner Factory ============
|
|
360
|
+
export function createRunner(app, projectPath, projectId, callbacks) {
|
|
361
|
+
// Use explicit appType if provided, otherwise detect
|
|
362
|
+
if (app.appType === 'static-frontend') {
|
|
363
|
+
return new StaticFrontendRunner(app, projectPath, projectId, callbacks);
|
|
364
|
+
}
|
|
365
|
+
if (app.appType === 'backend') {
|
|
366
|
+
return new BackendRunner(app, projectPath, projectId, callbacks);
|
|
367
|
+
}
|
|
368
|
+
// Auto-detect based on framework and other indicators
|
|
369
|
+
const framework = app.framework?.toLowerCase() || '';
|
|
370
|
+
const type = app.type?.toLowerCase() || '';
|
|
371
|
+
// Static frontend apps
|
|
372
|
+
if (framework.includes('react') ||
|
|
373
|
+
framework.includes('vue') ||
|
|
374
|
+
framework.includes('angular') ||
|
|
375
|
+
framework.includes('svelte') ||
|
|
376
|
+
(framework.includes('next') && type === 'http') ||
|
|
377
|
+
app.name.toLowerCase().includes('frontend') ||
|
|
378
|
+
app.name.toLowerCase().includes('client')) {
|
|
379
|
+
return new StaticFrontendRunner(app, projectPath, projectId, callbacks);
|
|
380
|
+
}
|
|
381
|
+
// Backend apps (Express, Fastify, etc.)
|
|
382
|
+
return new BackendRunner(app, projectPath, projectId, callbacks);
|
|
383
|
+
}
|