@exreve/exk 1.0.54 → 1.0.56
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 -1
- package/dist/ttc-cli.tar.gz +0 -0
- package/package.json +1 -1
- package/dist/agentLogger.js +0 -143
- package/dist/agentSession.js +0 -1455
- package/dist/app-child.js +0 -1038
- package/dist/appHandlers.js +0 -142
- package/dist/appManager.js +0 -212
- package/dist/appRunner.js +0 -383
- package/dist/benchmark-startup.js +0 -347
- package/dist/cloudflaredHandlers.js +0 -279
- package/dist/containerHandlers.js +0 -193
- package/dist/fsHandlers.js +0 -86
- package/dist/githubHandlers.js +0 -525
- package/dist/index.js +0 -1262
- package/dist/moduleMcpServer.js +0 -284
- package/dist/openaiAdapter.js +0 -181
- package/dist/projectAnalyzer.js +0 -330
- package/dist/projectManager.js +0 -69
- package/dist/runnerGenerator.js +0 -210
- package/dist/sessionHandlers.js +0 -271
- package/dist/skills/index.js +0 -117
- package/dist/transferService.js +0 -284
- package/dist/updateHandlers.js +0 -82
- package/dist/updater.js +0 -422
package/dist/projectAnalyzer.js
DELETED
|
@@ -1,330 +0,0 @@
|
|
|
1
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
2
|
-
import fs from 'fs/promises';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { agentSessionManager } from './agentSession.js';
|
|
5
|
-
const CONFIG_FILENAME = '.claude-voice.json';
|
|
6
|
-
/**
|
|
7
|
-
* Analyze project using Claude AI and generate structured JSON configuration
|
|
8
|
-
*/
|
|
9
|
-
export async function analyzeProjectWithClaude(projectPath, projectName) {
|
|
10
|
-
// Create a temporary session ID for analysis
|
|
11
|
-
const analysisSessionId = `analysis-${uuidv4()}`;
|
|
12
|
-
// Build the analysis prompt with structured output requirements
|
|
13
|
-
const analysisPrompt = `Analyze the project structure at ${projectPath} and generate a comprehensive project configuration.
|
|
14
|
-
|
|
15
|
-
CRITICAL: You must identify ALL applications/services in this project. Each app will get its own TypeScript runner file and entry in the JSON config.
|
|
16
|
-
|
|
17
|
-
Please examine:
|
|
18
|
-
1. Project files and directory structure (use Read/Glob tools to explore)
|
|
19
|
-
2. Framework and technology stack (React, Express, FastAPI, Django, etc.)
|
|
20
|
-
3. Apps (applications/services) - each app can be at root or in subdirectories
|
|
21
|
-
4. For each app, identify:
|
|
22
|
-
- App NAME: unique identifier for the app
|
|
23
|
-
- App TYPE: "static-frontend" (React/Vue/Angular/Svelte/Next.js static builds) or "backend" (Express/Fastify/FastAPI/Django/etc.)
|
|
24
|
-
- How to BUILD it (buildCommand - REQUIRED for static frontends)
|
|
25
|
-
- How to START it (startCommand - REQUIRED for all apps)
|
|
26
|
-
- How to STOP it (stopCommand - optional, defaults to killing process)
|
|
27
|
-
- How to RESTART it (restartCommand - optional, defaults to stop + start)
|
|
28
|
-
- Port number (port - REQUIRED for HTTP apps)
|
|
29
|
-
- Protocol (http/https/ws/etc.)
|
|
30
|
-
- HTTP endpoints/routes (endpoints array)
|
|
31
|
-
- Framework name (react/express/fastify/etc.)
|
|
32
|
-
- Directory location (relative to project root, empty string "" for root app)
|
|
33
|
-
- Build output directory (buildDir - for static frontends: dist/build/public)
|
|
34
|
-
- Environment variables (env object)
|
|
35
|
-
- Health check URL or command
|
|
36
|
-
5. Check package.json scripts, Makefile, docker-compose.yml, Procfile, vite.config.js, next.config.js, etc. for commands
|
|
37
|
-
6. Detect HTTP endpoints by examining route files, controllers, or API definitions
|
|
38
|
-
7. For static frontends, identify the build output directory (usually dist, build, or public)
|
|
39
|
-
8. Look for multiple apps in monorepos or multi-app projects
|
|
40
|
-
|
|
41
|
-
IMPORTANT OUTPUT REQUIREMENTS:
|
|
42
|
-
- You must output a JSON file named ".talk-to-code.json" in the project root
|
|
43
|
-
- The JSON must contain ALL apps/services found in the project
|
|
44
|
-
- Each app will be wrapped with a TypeScript runner file (generated automatically)
|
|
45
|
-
- Apps must be configured to work with the runner system
|
|
46
|
-
|
|
47
|
-
Output ONLY valid JSON matching this exact structure (no markdown, no code blocks, no explanations, just pure JSON):
|
|
48
|
-
|
|
49
|
-
{
|
|
50
|
-
"version": "1.0.0",
|
|
51
|
-
"projectName": "${projectName}",
|
|
52
|
-
"description": "Description of the project",
|
|
53
|
-
"apps": [
|
|
54
|
-
{
|
|
55
|
-
"name": "app-name",
|
|
56
|
-
"description": "Optional description",
|
|
57
|
-
"type": "http",
|
|
58
|
-
"port": 3000,
|
|
59
|
-
"protocol": "http",
|
|
60
|
-
"directory": "relative/path/to/app",
|
|
61
|
-
"framework": "react",
|
|
62
|
-
"appType": "static-frontend",
|
|
63
|
-
"buildDir": "dist",
|
|
64
|
-
"endpoints": ["/", "/api", "/api/v1"],
|
|
65
|
-
"env": {},
|
|
66
|
-
"healthCheck": {
|
|
67
|
-
"url": "http://localhost:3000/health"
|
|
68
|
-
},
|
|
69
|
-
"buildCommand": "npm run build",
|
|
70
|
-
"startCommand": "npm start",
|
|
71
|
-
"stopCommand": "pkill -f 'node.*start'",
|
|
72
|
-
"restartCommand": null,
|
|
73
|
-
"dependencies": []
|
|
74
|
-
}
|
|
75
|
-
],
|
|
76
|
-
"directories": ["subdirectory1", "subdirectory2"],
|
|
77
|
-
"metadata": {
|
|
78
|
-
"analyzedAt": "${new Date().toISOString()}",
|
|
79
|
-
"analyzedBy": "talk-to-code-agent",
|
|
80
|
-
"framework": "detected-framework",
|
|
81
|
-
"language": "javascript",
|
|
82
|
-
"packageManager": "npm"
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
CRITICAL INSTRUCTIONS:
|
|
87
|
-
1. Output ONLY the JSON object, nothing else
|
|
88
|
-
2. No markdown formatting, no code blocks, no explanations
|
|
89
|
-
3. Use Read/Glob tools to examine the project structure thoroughly
|
|
90
|
-
4. Extract actual port numbers, commands, and configurations from files
|
|
91
|
-
5. startCommand is REQUIRED for each app - must be the actual command to start the app
|
|
92
|
-
6. stopCommand and restartCommand are optional (can be null)
|
|
93
|
-
7. appType MUST be "static-frontend" for React/Vue/Angular/Svelte/Next.js static builds, or "backend" for Express/Fastify/FastAPI/Django/etc.
|
|
94
|
-
8. buildDir MUST be specified for static frontends (usually "dist", "build", or "public")
|
|
95
|
-
9. port is REQUIRED for HTTP apps
|
|
96
|
-
10. endpoints should be an array of HTTP routes (e.g., ["/", "/api", "/api/v1"])
|
|
97
|
-
11. directory should be relative path from project root, empty string "" for root app
|
|
98
|
-
12. Be thorough and accurate - identify ALL apps/services in the project
|
|
99
|
-
13. Each app will get a TypeScript runner file ({appName}_runner.ts) generated automatically
|
|
100
|
-
14. The runner will wrap the app to collect logs, stats, and provide unified control
|
|
101
|
-
15. After generating the JSON, write it to ".claude-voice.json" file in the project root using the Write tool`;
|
|
102
|
-
let config = null;
|
|
103
|
-
let lastAssistantMessage = '';
|
|
104
|
-
let errorOccurred = false;
|
|
105
|
-
let foundJson = false;
|
|
106
|
-
const assistantMessages = [];
|
|
107
|
-
try {
|
|
108
|
-
// Create temporary session handler
|
|
109
|
-
await agentSessionManager.createSession({
|
|
110
|
-
sessionId: analysisSessionId,
|
|
111
|
-
projectPath,
|
|
112
|
-
onOutput: (output) => {
|
|
113
|
-
// Capture assistant messages to extract JSON
|
|
114
|
-
if (output.type === 'assistant') {
|
|
115
|
-
const data = typeof output.data === 'string' ? output.data : JSON.stringify(output.data);
|
|
116
|
-
assistantMessages.push(data);
|
|
117
|
-
lastAssistantMessage = data;
|
|
118
|
-
}
|
|
119
|
-
// Check for file writes (Claude might write the JSON file directly)
|
|
120
|
-
if (output.type === 'tool_result' && output.metadata?.toolName === 'Write') {
|
|
121
|
-
const toolResult = output.metadata.toolResult;
|
|
122
|
-
if (toolResult && typeof toolResult === 'object' && 'file_path' in toolResult) {
|
|
123
|
-
const filePath = toolResult.file_path;
|
|
124
|
-
if (filePath && filePath.includes('.talk-to-code.json')) {
|
|
125
|
-
// Claude wrote the config file, mark as found
|
|
126
|
-
foundJson = true;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
if (output.type === 'result' && output.metadata?.isError) {
|
|
131
|
-
errorOccurred = true;
|
|
132
|
-
}
|
|
133
|
-
},
|
|
134
|
-
onError: (error) => {
|
|
135
|
-
console.error(`Analysis session error: ${error}`);
|
|
136
|
-
errorOccurred = true;
|
|
137
|
-
},
|
|
138
|
-
onComplete: () => {
|
|
139
|
-
// Session completed
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
// Send analysis prompt
|
|
143
|
-
await agentSessionManager.sendPrompt(analysisSessionId, analysisPrompt, [], {
|
|
144
|
-
sessionId: analysisSessionId,
|
|
145
|
-
projectPath,
|
|
146
|
-
onOutput: (output) => {
|
|
147
|
-
// Capture assistant messages
|
|
148
|
-
if (output.type === 'assistant') {
|
|
149
|
-
const data = typeof output.data === 'string' ? output.data : JSON.stringify(output.data);
|
|
150
|
-
assistantMessages.push(data);
|
|
151
|
-
lastAssistantMessage = data;
|
|
152
|
-
}
|
|
153
|
-
// Also check for file writes (Claude might write the JSON file directly)
|
|
154
|
-
if (output.type === 'tool_result' && output.metadata?.toolName === 'Write') {
|
|
155
|
-
// Claude wrote a file, check if it's the config file
|
|
156
|
-
const toolResult = output.metadata.toolResult;
|
|
157
|
-
if (toolResult && typeof toolResult === 'object' && 'file_path' in toolResult) {
|
|
158
|
-
const filePath = toolResult.file_path;
|
|
159
|
-
if (filePath && filePath.includes('.talk-to-code.json')) {
|
|
160
|
-
// Claude wrote the config file, try to read it
|
|
161
|
-
setTimeout(async () => {
|
|
162
|
-
try {
|
|
163
|
-
const configPath = path.join(projectPath, '.talk-to-code.json');
|
|
164
|
-
const content = await fs.readFile(configPath, 'utf-8');
|
|
165
|
-
const parsed = JSON.parse(content);
|
|
166
|
-
if (parsed && parsed.apps) {
|
|
167
|
-
config = parsed;
|
|
168
|
-
foundJson = true;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
catch (error) {
|
|
172
|
-
// Ignore read errors
|
|
173
|
-
}
|
|
174
|
-
}, 1000);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
},
|
|
179
|
-
onError: (error) => {
|
|
180
|
-
console.error(`Analysis error: ${error}`);
|
|
181
|
-
errorOccurred = true;
|
|
182
|
-
},
|
|
183
|
-
onComplete: () => {
|
|
184
|
-
// Analysis complete
|
|
185
|
-
}
|
|
186
|
-
});
|
|
187
|
-
// Wait for Claude to process (give it time to read files and generate response)
|
|
188
|
-
// Use exponential backoff: start at 500ms, double each time, cap at 5s, timeout at 90s
|
|
189
|
-
const configFilePath = path.join(projectPath, CONFIG_FILENAME);
|
|
190
|
-
const POLL_TIMEOUT_MS = 90_000;
|
|
191
|
-
const POLL_START_MS = 500;
|
|
192
|
-
const POLL_MAX_MS = 5_000;
|
|
193
|
-
let elapsed = 0;
|
|
194
|
-
let interval = POLL_START_MS;
|
|
195
|
-
while (elapsed < POLL_TIMEOUT_MS) {
|
|
196
|
-
await new Promise(resolve => setTimeout(resolve, interval));
|
|
197
|
-
elapsed += interval;
|
|
198
|
-
interval = Math.min(interval * 2, POLL_MAX_MS);
|
|
199
|
-
// Check if Claude wrote the config file directly
|
|
200
|
-
try {
|
|
201
|
-
await fs.access(configFilePath);
|
|
202
|
-
const fileContent = await fs.readFile(configFilePath, 'utf-8');
|
|
203
|
-
const parsed = JSON.parse(fileContent);
|
|
204
|
-
if (parsed && parsed.apps) {
|
|
205
|
-
config = parsed;
|
|
206
|
-
foundJson = true;
|
|
207
|
-
break;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
catch {
|
|
211
|
-
// File doesn't exist yet or invalid JSON, continue waiting
|
|
212
|
-
}
|
|
213
|
-
// Check if we have a complete JSON response in messages
|
|
214
|
-
if (lastAssistantMessage && (lastAssistantMessage.includes('"version"') || lastAssistantMessage.includes('"apps"') || lastAssistantMessage.includes('"services"'))) {
|
|
215
|
-
foundJson = true;
|
|
216
|
-
break;
|
|
217
|
-
}
|
|
218
|
-
// Check if session completed (result message received)
|
|
219
|
-
if (errorOccurred) {
|
|
220
|
-
break;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
// If config was read from file, use it instead of parsing messages
|
|
224
|
-
if (!config || !foundJson) {
|
|
225
|
-
// Try to extract JSON from the assistant messages (check all messages, latest first)
|
|
226
|
-
for (let i = assistantMessages.length - 1; i >= 0; i--) {
|
|
227
|
-
let jsonStr = assistantMessages[i].trim();
|
|
228
|
-
// Remove markdown code blocks if present
|
|
229
|
-
jsonStr = jsonStr.replace(/^```json\s*/i, '').replace(/^```\s*/, '').replace(/\s*```$/, '');
|
|
230
|
-
jsonStr = jsonStr.trim();
|
|
231
|
-
// Try to extract JSON object if wrapped in text
|
|
232
|
-
const jsonMatch = jsonStr.match(/\{[\s\S]*\}/);
|
|
233
|
-
if (jsonMatch) {
|
|
234
|
-
jsonStr = jsonMatch[0];
|
|
235
|
-
}
|
|
236
|
-
try {
|
|
237
|
-
const parsedConfig = JSON.parse(jsonStr);
|
|
238
|
-
config = parsedConfig;
|
|
239
|
-
foundJson = true;
|
|
240
|
-
break;
|
|
241
|
-
}
|
|
242
|
-
catch (parseError) {
|
|
243
|
-
// Try next message
|
|
244
|
-
continue;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
// Validate and ensure required fields if config was loaded
|
|
249
|
-
if (config && foundJson) {
|
|
250
|
-
// Validate and ensure required fields
|
|
251
|
-
if (!config.version)
|
|
252
|
-
config.version = '1.0.0';
|
|
253
|
-
if (!config.projectName)
|
|
254
|
-
config.projectName = projectName;
|
|
255
|
-
if (!config.apps)
|
|
256
|
-
config.apps = [];
|
|
257
|
-
if (!config.directories)
|
|
258
|
-
config.directories = [];
|
|
259
|
-
// Migrate old "services" to "apps" if present
|
|
260
|
-
if (config.services && !config.apps) {
|
|
261
|
-
config.apps = config.services;
|
|
262
|
-
}
|
|
263
|
-
// Ensure each app has a startCommand
|
|
264
|
-
config.apps = config.apps.map(app => ({
|
|
265
|
-
...app,
|
|
266
|
-
startCommand: app.startCommand || 'echo "No start command configured"',
|
|
267
|
-
}));
|
|
268
|
-
if (!config.metadata) {
|
|
269
|
-
config.metadata = {
|
|
270
|
-
analyzedAt: new Date().toISOString(),
|
|
271
|
-
analyzedBy: 'talk-to-code-agent'
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
else {
|
|
275
|
-
config.metadata.analyzedAt = new Date().toISOString();
|
|
276
|
-
config.metadata.analyzedBy = 'talk-to-code-agent';
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
// Clean up session
|
|
280
|
-
try {
|
|
281
|
-
await agentSessionManager.deleteSession(analysisSessionId);
|
|
282
|
-
}
|
|
283
|
-
catch (cleanupError) {
|
|
284
|
-
console.warn(`Failed to cleanup analysis session:`, cleanupError);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
catch (error) {
|
|
288
|
-
console.error(`Error during Claude analysis:`, error.message);
|
|
289
|
-
errorOccurred = true;
|
|
290
|
-
// Try to cleanup session
|
|
291
|
-
try {
|
|
292
|
-
await agentSessionManager.deleteSession(analysisSessionId);
|
|
293
|
-
}
|
|
294
|
-
catch { }
|
|
295
|
-
}
|
|
296
|
-
// If analysis failed or no config extracted, throw error (caller can handle fallback)
|
|
297
|
-
if (!config || errorOccurred) {
|
|
298
|
-
throw new Error(`Claude analysis failed or incomplete. Last message: ${lastAssistantMessage.substring(0, 200)}`);
|
|
299
|
-
}
|
|
300
|
-
return config;
|
|
301
|
-
}
|
|
302
|
-
/**
|
|
303
|
-
* Read project configuration from file
|
|
304
|
-
*/
|
|
305
|
-
export async function getProjectConfig(projectPath) {
|
|
306
|
-
try {
|
|
307
|
-
const configPath = path.join(projectPath, CONFIG_FILENAME);
|
|
308
|
-
const content = await fs.readFile(configPath, 'utf-8');
|
|
309
|
-
const config = JSON.parse(content);
|
|
310
|
-
// Migrate old "services" to "apps" if present
|
|
311
|
-
if (config.services && !config.apps) {
|
|
312
|
-
config.apps = config.services;
|
|
313
|
-
}
|
|
314
|
-
return config;
|
|
315
|
-
}
|
|
316
|
-
catch (error) {
|
|
317
|
-
if (error.code === 'ENOENT') {
|
|
318
|
-
return null; // Config file doesn't exist yet
|
|
319
|
-
}
|
|
320
|
-
throw error;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
/**
|
|
324
|
-
* Save project configuration to file
|
|
325
|
-
*/
|
|
326
|
-
export async function saveProjectConfig(projectPath, config) {
|
|
327
|
-
const configPath = path.join(projectPath, CONFIG_FILENAME);
|
|
328
|
-
const content = JSON.stringify(config, null, 2);
|
|
329
|
-
await fs.writeFile(configPath, content, 'utf-8');
|
|
330
|
-
}
|
package/dist/projectManager.js
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import fs from 'fs/promises';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
const toAbsolutePath = (p) => path.isAbsolute(p) ? p : path.resolve(p);
|
|
4
|
-
export async function createProject(request) {
|
|
5
|
-
try {
|
|
6
|
-
const { path: projectPath, sourcePath } = request;
|
|
7
|
-
const absolutePath = toAbsolutePath(projectPath);
|
|
8
|
-
// Check if source path exists (if linking)
|
|
9
|
-
if (sourcePath) {
|
|
10
|
-
const absoluteSourcePath = toAbsolutePath(sourcePath);
|
|
11
|
-
try {
|
|
12
|
-
const sourceStat = await fs.stat(absoluteSourcePath);
|
|
13
|
-
if (!sourceStat.isDirectory()) {
|
|
14
|
-
return { success: false, error: 'Source path is not a directory' };
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
return { success: false, error: 'Source path does not exist' };
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
// Check if project directory already exists
|
|
22
|
-
try {
|
|
23
|
-
const stat = await fs.stat(absolutePath);
|
|
24
|
-
if (!stat.isDirectory()) {
|
|
25
|
-
return { success: false, error: 'Path exists but is not a directory' };
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
// Directory doesn't exist, create it
|
|
30
|
-
try {
|
|
31
|
-
await fs.mkdir(absolutePath, { recursive: true });
|
|
32
|
-
}
|
|
33
|
-
catch (error) {
|
|
34
|
-
return { success: false, error: `Failed to create directory: ${error.message}` };
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
// If linking to source, create symlink or copy (for now, just validate)
|
|
38
|
-
// In a real implementation, you might want to create a symlink or copy files
|
|
39
|
-
if (sourcePath) {
|
|
40
|
-
// For now, we just validate that both paths exist
|
|
41
|
-
// The actual linking can be done by the application logic
|
|
42
|
-
}
|
|
43
|
-
return { success: true, actualPath: absolutePath };
|
|
44
|
-
}
|
|
45
|
-
catch (error) {
|
|
46
|
-
return { success: false, error: error.message };
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
export async function deleteProject(projectPath) {
|
|
50
|
-
try {
|
|
51
|
-
const absolutePath = toAbsolutePath(projectPath);
|
|
52
|
-
try {
|
|
53
|
-
const stat = await fs.stat(absolutePath);
|
|
54
|
-
if (!stat.isDirectory()) {
|
|
55
|
-
return { success: false, error: 'Path is not a directory' };
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
// Directory doesn't exist, consider it deleted
|
|
60
|
-
return { success: true };
|
|
61
|
-
}
|
|
62
|
-
// Don't delete the directory on device - just succeed
|
|
63
|
-
// The directory will remain on the filesystem
|
|
64
|
-
return { success: true };
|
|
65
|
-
}
|
|
66
|
-
catch (error) {
|
|
67
|
-
return { success: false, error: error.message };
|
|
68
|
-
}
|
|
69
|
-
}
|
package/dist/runnerGenerator.js
DELETED
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Generate TypeScript runner code for an app
|
|
3
|
-
*/
|
|
4
|
-
export function generateRunnerCode(app, projectPath) {
|
|
5
|
-
const appType = app.appType || (app.framework?.toLowerCase().includes('react') ||
|
|
6
|
-
app.framework?.toLowerCase().includes('vue') ||
|
|
7
|
-
app.framework?.toLowerCase().includes('angular') ||
|
|
8
|
-
app.framework?.toLowerCase().includes('svelte') ? 'static-frontend' : 'backend');
|
|
9
|
-
if (appType === 'static-frontend') {
|
|
10
|
-
return generateStaticFrontendRunner(app, projectPath);
|
|
11
|
-
}
|
|
12
|
-
else {
|
|
13
|
-
return generateBackendRunner(app, projectPath);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
function generateStaticFrontendRunner(app, _projectPath) {
|
|
17
|
-
const appName = app.name.replace(/[^a-zA-Z0-9_-]/g, '_'); // Sanitize app name for filename
|
|
18
|
-
const port = app.port || 3000;
|
|
19
|
-
const buildDir = app.buildDir || 'dist';
|
|
20
|
-
const appDir = app.directory || '';
|
|
21
|
-
return `import Fastify from 'fastify'
|
|
22
|
-
import fastifyStatic from '@fastify/static'
|
|
23
|
-
import path from 'path'
|
|
24
|
-
import fs from 'fs/promises'
|
|
25
|
-
import { fileURLToPath } from 'url'
|
|
26
|
-
import { dirname } from 'path'
|
|
27
|
-
|
|
28
|
-
const __filename = fileURLToPath(import.meta.url)
|
|
29
|
-
const __dirname = dirname(__filename)
|
|
30
|
-
const PORT = ${port}
|
|
31
|
-
const BUILD_DIR = path.join(__dirname, ${appDir ? `'${appDir}', ` : ''}'${buildDir}')
|
|
32
|
-
|
|
33
|
-
async function start() {
|
|
34
|
-
// Check if build directory exists
|
|
35
|
-
try {
|
|
36
|
-
await fs.access(BUILD_DIR)
|
|
37
|
-
} catch {
|
|
38
|
-
console.error(\`✗ Build directory not found: \${BUILD_DIR}\`)
|
|
39
|
-
console.error(' Please build the project first using: ' + (process.env.BUILD_COMMAND || 'npm run build'))
|
|
40
|
-
process.exit(1)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const fastify = Fastify({
|
|
44
|
-
logger: {
|
|
45
|
-
level: 'info',
|
|
46
|
-
transport: {
|
|
47
|
-
target: 'pino-pretty',
|
|
48
|
-
options: {
|
|
49
|
-
translateTime: 'HH:MM:ss Z',
|
|
50
|
-
ignore: 'pid,hostname',
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
// Register static file serving
|
|
57
|
-
await fastify.register(fastifyStatic, {
|
|
58
|
-
root: BUILD_DIR,
|
|
59
|
-
prefix: '/',
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
// Request logging
|
|
63
|
-
fastify.addHook('onRequest', async (request, reply) => {
|
|
64
|
-
const logLine = \`[\${new Date().toISOString()}] \${request.method} \${request.url} - \${reply.statusCode}\\n\`
|
|
65
|
-
await fs.appendFile('${appName}_runner.log', logLine, 'utf-8').catch(() => {})
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
// Error handling
|
|
69
|
-
fastify.setErrorHandler(async (error, request, reply) => {
|
|
70
|
-
const logLine = \`[\${new Date().toISOString()}] ERROR: \${error.message} - \${request.method} \${request.url}\\n\`
|
|
71
|
-
await fs.appendFile('${appName}_runner.log', logLine, 'utf-8').catch(() => {})
|
|
72
|
-
reply.status(500).send({ error: 'Internal Server Error' })
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
await fastify.listen({ port: PORT, host: '0.0.0.0' })
|
|
77
|
-
console.log(\`✓ Static server started on port \${PORT}\`)
|
|
78
|
-
console.log(\` Serving from: \${BUILD_DIR}\`)
|
|
79
|
-
} catch (error: any) {
|
|
80
|
-
console.error(\`✗ Failed to start server:\`, error.message)
|
|
81
|
-
process.exit(1)
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Handle graceful shutdown
|
|
86
|
-
process.on('SIGTERM', async () => {
|
|
87
|
-
console.log('Shutting down...')
|
|
88
|
-
await fastify.close()
|
|
89
|
-
process.exit(0)
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
process.on('SIGINT', async () => {
|
|
93
|
-
console.log('Shutting down...')
|
|
94
|
-
await fastify.close()
|
|
95
|
-
process.exit(0)
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
start()
|
|
99
|
-
`;
|
|
100
|
-
}
|
|
101
|
-
function generateBackendRunner(app, _projectPath) {
|
|
102
|
-
const appName = app.name.replace(/[^a-zA-Z0-9_-]/g, '_'); // Sanitize app name for filename
|
|
103
|
-
const appDir = app.directory || '';
|
|
104
|
-
const startCommand = app.startCommand;
|
|
105
|
-
const envVars = app.env || {};
|
|
106
|
-
const envString = Object.entries(envVars)
|
|
107
|
-
.map(([key, value]) => ` ${key}: '${value}',`)
|
|
108
|
-
.join('\n');
|
|
109
|
-
return `import { spawn } from 'child_process'
|
|
110
|
-
import fs from 'fs/promises'
|
|
111
|
-
import path from 'path'
|
|
112
|
-
import { fileURLToPath } from 'url'
|
|
113
|
-
import { dirname } from 'path'
|
|
114
|
-
|
|
115
|
-
const __filename = fileURLToPath(import.meta.url)
|
|
116
|
-
const __dirname = dirname(__filename)
|
|
117
|
-
const APP_DIR = path.join(__dirname, ${appDir ? `'${appDir}'` : 'undefined'})
|
|
118
|
-
const START_COMMAND = '${startCommand}'
|
|
119
|
-
|
|
120
|
-
const env = {
|
|
121
|
-
...process.env,
|
|
122
|
-
${envString}
|
|
123
|
-
NODE_ENV: process.env.NODE_ENV || 'development',
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
let childProcess: ReturnType<typeof spawn> | null = null
|
|
127
|
-
|
|
128
|
-
async function start() {
|
|
129
|
-
const workingDir = APP_DIR || process.cwd()
|
|
130
|
-
|
|
131
|
-
console.log(\`Starting backend app: ${app.name}\`)
|
|
132
|
-
console.log(\` Directory: \${workingDir}\`)
|
|
133
|
-
console.log(\` Command: \${START_COMMAND}\`)
|
|
134
|
-
|
|
135
|
-
const isWin = process.platform === 'win32'
|
|
136
|
-
const shell = isWin ? (process.env.COMSPEC || 'cmd.exe') : 'sh'
|
|
137
|
-
const args = isWin ? ['/c', START_COMMAND] : ['-c', START_COMMAND]
|
|
138
|
-
childProcess = spawn(shell, args, {
|
|
139
|
-
cwd: workingDir,
|
|
140
|
-
env,
|
|
141
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
if (!childProcess.pid) {
|
|
145
|
-
console.error('✗ Failed to spawn process')
|
|
146
|
-
process.exit(1)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
console.log(\`✓ Backend started with PID: \${childProcess.pid}\`)
|
|
150
|
-
|
|
151
|
-
// Capture stdout
|
|
152
|
-
childProcess.stdout?.on('data', async (data: Buffer) => {
|
|
153
|
-
const output = data.toString()
|
|
154
|
-
const logLine = \`[\${new Date().toISOString()}] [STDOUT] \${output}\`
|
|
155
|
-
await fs.appendFile('${appName}_runner.log', logLine, 'utf-8').catch(() => {})
|
|
156
|
-
process.stdout.write(output)
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
// Capture stderr
|
|
160
|
-
childProcess.stderr?.on('data', async (data: Buffer) => {
|
|
161
|
-
const output = data.toString()
|
|
162
|
-
const logLine = \`[\${new Date().toISOString()}] [STDERR] \${output}\`
|
|
163
|
-
await fs.appendFile('${appName}_runner.log', logLine, 'utf-8').catch(() => {})
|
|
164
|
-
process.stderr.write(output)
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
// Handle process exit
|
|
168
|
-
childProcess.on('exit', async (code) => {
|
|
169
|
-
const logLine = \`[\${new Date().toISOString()}] Process exited with code \${code}\\n\`
|
|
170
|
-
await fs.appendFile('${appName}_runner.log', logLine, 'utf-8').catch(() => {})
|
|
171
|
-
console.log(\`Process exited with code \${code}\`)
|
|
172
|
-
childProcess = null
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
// Handle process errors
|
|
176
|
-
childProcess.on('error', async (error) => {
|
|
177
|
-
const logLine = \`[\${new Date().toISOString()}] Process error: \${error.message}\\n\`
|
|
178
|
-
await fs.appendFile('${appName}_runner.log', logLine, 'utf-8').catch(() => {})
|
|
179
|
-
console.error(\`Process error: \${error.message}\`)
|
|
180
|
-
})
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Handle graceful shutdown
|
|
184
|
-
process.on('SIGTERM', async () => {
|
|
185
|
-
console.log('Shutting down...')
|
|
186
|
-
if (childProcess) {
|
|
187
|
-
childProcess.kill('SIGTERM')
|
|
188
|
-
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
189
|
-
if (childProcess) {
|
|
190
|
-
childProcess.kill('SIGKILL')
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
process.exit(0)
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
process.on('SIGINT', async () => {
|
|
197
|
-
console.log('Shutting down...')
|
|
198
|
-
if (childProcess) {
|
|
199
|
-
childProcess.kill('SIGTERM')
|
|
200
|
-
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
201
|
-
if (childProcess) {
|
|
202
|
-
childProcess.kill('SIGKILL')
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
process.exit(0)
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
start()
|
|
209
|
-
`;
|
|
210
|
-
}
|