@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/appRunner.ts DELETED
@@ -1,475 +0,0 @@
1
- import { spawn, ChildProcess } from 'child_process'
2
- import fs from 'fs/promises'
3
- import path from 'path'
4
- import os from 'os'
5
- import type { ProjectApp } from './shared/types.js'
6
-
7
- /** Cross-platform: run a shell command (sh -c on Unix, cmd /c on Windows) */
8
- function shellSpawnOpts(command: string): { shell: string; args: string[] } {
9
- if (process.platform === 'win32') {
10
- return { shell: process.env.COMSPEC || 'cmd.exe', args: ['/c', command] }
11
- }
12
- return { shell: 'sh', args: ['-c', command] }
13
- }
14
- import Fastify, { FastifyInstance } from 'fastify'
15
- import fastifyStatic from '@fastify/static'
16
-
17
- // ============ Types ============
18
-
19
- export interface RunnerStats {
20
- startTime: number
21
- requests: number
22
- errors: number
23
- lastRequestTime?: number
24
- memoryUsage?: NodeJS.MemoryUsage
25
- }
26
-
27
- export interface RunnerOutput {
28
- type: 'stdout' | 'stderr' | 'system'
29
- data: string
30
- timestamp: number
31
- }
32
-
33
- export interface RunnerCallbacks {
34
- onOutput?: (output: RunnerOutput) => void
35
- onError?: (error: string) => void
36
- onExit?: (code: number | null) => void
37
- onStats?: (stats: RunnerStats) => void
38
- }
39
-
40
- // ============ Base Runner ============
41
-
42
- export abstract class BaseRunner {
43
- protected app: ProjectApp
44
- protected projectPath: string
45
- protected projectId: string
46
- protected callbacks: RunnerCallbacks
47
- protected stats: RunnerStats
48
- protected process: ChildProcess | null = null
49
- protected logFile: string
50
-
51
- constructor(
52
- app: ProjectApp,
53
- projectPath: string,
54
- projectId: string,
55
- callbacks: RunnerCallbacks
56
- ) {
57
- this.app = app
58
- this.projectPath = projectPath
59
- this.projectId = projectId
60
- this.callbacks = callbacks
61
- this.stats = {
62
- startTime: Date.now(),
63
- requests: 0,
64
- errors: 0,
65
- }
66
- this.logFile = path.join(os.homedir(), '.talk-to-code', 'app-logs', `${projectId}-${app.name}.log`)
67
- }
68
-
69
- abstract start(): Promise<{ success: boolean; pid?: number; error?: string }>
70
- abstract stop(): Promise<{ success: boolean; error?: string }>
71
- abstract getStats(): RunnerStats
72
-
73
- protected async writeLog(type: 'stdout' | 'stderr' | 'system', data: string): Promise<void> {
74
- const timestamp = new Date().toISOString()
75
- const logLine = `[${timestamp}] [${type.toUpperCase()}] ${data}\n`
76
-
77
- try {
78
- await fs.appendFile(this.logFile, logLine, 'utf-8')
79
- } catch (error) {
80
- // Ignore log write errors
81
- }
82
-
83
- this.callbacks.onOutput?.({
84
- type,
85
- data,
86
- timestamp: Date.now(),
87
- })
88
- }
89
-
90
- protected updateStats(): void {
91
- if (this.process) {
92
- try {
93
- const memUsage = process.memoryUsage()
94
- this.stats.memoryUsage = memUsage
95
- } catch {
96
- // Ignore
97
- }
98
- }
99
- this.callbacks.onStats?.(this.stats)
100
- }
101
- }
102
-
103
- // ============ Static Frontend Runner ============
104
-
105
- export class StaticFrontendRunner extends BaseRunner {
106
- private fastify: FastifyInstance | null = null
107
- private port: number
108
-
109
- constructor(
110
- app: ProjectApp,
111
- projectPath: string,
112
- projectId: string,
113
- callbacks: RunnerCallbacks
114
- ) {
115
- super(app, projectPath, projectId, callbacks)
116
- this.port = app.port || 3000
117
- }
118
-
119
- async start(): Promise<{ success: boolean; pid?: number; error?: string }> {
120
- try {
121
- await this.writeLog('system', `Starting static frontend server for ${this.app.name} on port ${this.port}`)
122
-
123
- // Determine build directory (use buildDir from config if provided)
124
- const buildDirName = this.app.buildDir || 'dist'
125
- const buildDir = this.app.directory
126
- ? path.join(this.projectPath, this.app.directory, buildDirName)
127
- : path.join(this.projectPath, buildDirName)
128
-
129
- // Check if specified build dir exists, fallback to dist, build, or public
130
- let staticDir = buildDir
131
- const dirsToTry = buildDirName !== 'dist' ? [buildDirName, 'dist', 'build', 'public'] : ['dist', 'build', 'public']
132
-
133
- let found = false
134
- for (const dirName of dirsToTry) {
135
- const testDir = this.app.directory
136
- ? path.join(this.projectPath, this.app.directory, dirName)
137
- : path.join(this.projectPath, dirName)
138
- try {
139
- await fs.access(testDir)
140
- staticDir = testDir
141
- found = true
142
- break
143
- } catch {
144
- continue
145
- }
146
- }
147
-
148
- if (!found) {
149
- return {
150
- success: false,
151
- error: `No build directory found. Tried: ${dirsToTry.join(', ')}`
152
- }
153
- }
154
-
155
- await this.writeLog('system', `Serving static files from: ${staticDir}`)
156
-
157
- // Create Fastify instance
158
- this.fastify = Fastify({
159
- logger: {
160
- level: 'info',
161
- transport: {
162
- target: 'pino-pretty',
163
- options: {
164
- translateTime: 'HH:MM:ss Z',
165
- ignore: 'pid,hostname',
166
- },
167
- },
168
- },
169
- })
170
-
171
- // Register static file serving
172
- await this.fastify.register(fastifyStatic, {
173
- root: staticDir,
174
- prefix: '/',
175
- })
176
-
177
- // Add request logging middleware
178
- this.fastify.addHook('onRequest', async (request, reply) => {
179
- this.stats.requests++
180
- this.stats.lastRequestTime = Date.now()
181
- this.updateStats()
182
-
183
- await this.writeLog('stdout', `${request.method} ${request.url} - ${reply.statusCode}`)
184
- })
185
-
186
- // Add error handling
187
- this.fastify.setErrorHandler(async (error: unknown, request, reply) => {
188
- this.stats.errors++
189
- this.updateStats()
190
-
191
- const errorMessage = error instanceof Error ? error.message : String(error)
192
- await this.writeLog('stderr', `Error: ${errorMessage} - ${request.method} ${request.url}`)
193
-
194
- reply.status(500).send({ error: 'Internal Server Error' })
195
- })
196
-
197
- // Start server
198
- await this.fastify.listen({ port: this.port, host: '0.0.0.0' })
199
-
200
- await this.writeLog('system', `Static server started successfully on port ${this.port}`)
201
-
202
- // Create a mock process for compatibility
203
- this.process = {
204
- pid: process.pid,
205
- kill: (signal?: NodeJS.Signals) => {
206
- this.stop()
207
- },
208
- } as ChildProcess
209
-
210
- return {
211
- success: true,
212
- pid: process.pid,
213
- }
214
- } catch (error: any) {
215
- await this.writeLog('stderr', `Failed to start static server: ${error.message}`)
216
- this.callbacks.onError?.(error.message)
217
- return {
218
- success: false,
219
- error: error.message || 'Failed to start static server'
220
- }
221
- }
222
- }
223
-
224
- async stop(): Promise<{ success: boolean; error?: string }> {
225
- try {
226
- if (this.fastify) {
227
- await this.writeLog('system', `Stopping static server for ${this.app.name}`)
228
- await this.fastify.close()
229
- this.fastify = null
230
- await this.writeLog('system', `Static server stopped`)
231
- this.callbacks.onExit?.(0)
232
- return { success: true }
233
- }
234
- return { success: true }
235
- } catch (error: any) {
236
- await this.writeLog('stderr', `Error stopping static server: ${error.message}`)
237
- return {
238
- success: false,
239
- error: error.message || 'Failed to stop static server'
240
- }
241
- }
242
- }
243
-
244
- getStats(): RunnerStats {
245
- return {
246
- ...this.stats,
247
- memoryUsage: process.memoryUsage(),
248
- }
249
- }
250
- }
251
-
252
- // ============ Backend Runner (Express/Fastify wrapper) ============
253
-
254
- export class BackendRunner extends BaseRunner {
255
- // process is inherited from BaseRunner as protected
256
-
257
- async start(): Promise<{ success: boolean; pid?: number; error?: string }> {
258
- try {
259
- await this.writeLog('system', `Starting backend app: ${this.app.name}`)
260
-
261
- // Determine working directory
262
- const workingDir = this.app.directory
263
- ? path.join(this.projectPath, this.app.directory)
264
- : this.projectPath
265
-
266
- // Check if directory exists
267
- try {
268
- await fs.access(workingDir)
269
- } catch {
270
- return {
271
- success: false,
272
- error: `App directory does not exist: ${this.app.directory || 'root'}`
273
- }
274
- }
275
-
276
- // Prepare environment variables
277
- const env = {
278
- ...process.env,
279
- ...this.app.env,
280
- NODE_ENV: this.app.env?.NODE_ENV || process.env.NODE_ENV || 'development',
281
- PORT: this.app.port?.toString() || process.env.PORT || '3000',
282
- }
283
-
284
- // Ensure log directory exists
285
- const logDir = path.dirname(this.logFile)
286
- await fs.mkdir(logDir, { recursive: true })
287
-
288
- // Spawn the backend process (cross-platform shell)
289
- const { shell, args } = shellSpawnOpts(this.app.startCommand)
290
- this.process = spawn(shell, args, {
291
- cwd: workingDir,
292
- env,
293
- stdio: ['ignore', 'pipe', 'pipe'],
294
- detached: false,
295
- })
296
-
297
- if (!this.process.pid) {
298
- return {
299
- success: false,
300
- error: 'Failed to spawn process'
301
- }
302
- }
303
-
304
- await this.writeLog('system', `Backend process started with PID: ${this.process.pid}`)
305
-
306
- // Capture stdout
307
- this.process.stdout?.on('data', async (data: Buffer) => {
308
- const output = data.toString()
309
- await this.writeLog('stdout', output)
310
- })
311
-
312
- // Capture stderr
313
- this.process.stderr?.on('data', async (data: Buffer) => {
314
- const output = data.toString()
315
- this.stats.errors++
316
- this.updateStats()
317
- await this.writeLog('stderr', output)
318
- })
319
-
320
- // Handle process exit
321
- this.process.on('exit', async (code) => {
322
- await this.writeLog('system', `Process exited with code ${code}`)
323
- this.process = null
324
- this.callbacks.onExit?.(code)
325
- })
326
-
327
- // Handle process errors
328
- this.process.on('error', async (error) => {
329
- this.stats.errors++
330
- this.updateStats()
331
- await this.writeLog('stderr', `Process error: ${error.message}`)
332
- this.callbacks.onError?.(error.message)
333
- })
334
-
335
- // Start stats collection interval
336
- const statsInterval = setInterval(() => {
337
- if (this.process) {
338
- this.updateStats()
339
- } else {
340
- clearInterval(statsInterval)
341
- }
342
- }, 5000)
343
-
344
- return {
345
- success: true,
346
- pid: this.process.pid,
347
- }
348
- } catch (error: any) {
349
- await this.writeLog('stderr', `Failed to start backend: ${error.message}`)
350
- this.callbacks.onError?.(error.message)
351
- return {
352
- success: false,
353
- error: error.message || 'Failed to start backend'
354
- }
355
- }
356
- }
357
-
358
- async stop(): Promise<{ success: boolean; error?: string }> {
359
- try {
360
- if (!this.process) {
361
- return { success: true }
362
- }
363
-
364
- await this.writeLog('system', `Stopping backend app: ${this.app.name}`)
365
-
366
- // Try custom stop command first
367
- if (this.app.stopCommand) {
368
- try {
369
- const workingDir = this.app.directory
370
- ? path.join(this.projectPath, this.app.directory)
371
- : this.projectPath
372
-
373
- const { shell: stopShell, args: stopArgs } = shellSpawnOpts(this.app.stopCommand)
374
- const stopProcess = spawn(stopShell, stopArgs, {
375
- cwd: workingDir,
376
- stdio: 'ignore',
377
- })
378
-
379
- // Wait for graceful shutdown
380
- await new Promise(resolve => setTimeout(resolve, 2000))
381
-
382
- // If still running, kill it
383
- if (this.process && this.process.pid) {
384
- try {
385
- process.kill(this.process.pid, 0) // Check if still alive
386
- this.process.kill('SIGTERM')
387
- await new Promise(resolve => setTimeout(resolve, 2000))
388
- if (this.process && this.process.pid) {
389
- try {
390
- process.kill(this.process.pid, 0)
391
- this.process.kill('SIGKILL')
392
- } catch {
393
- // Process already dead
394
- }
395
- }
396
- } catch {
397
- // Process already dead
398
- }
399
- }
400
- } catch (error) {
401
- // Fall back to killing process
402
- if (this.process) {
403
- this.process.kill('SIGTERM')
404
- await new Promise(resolve => setTimeout(resolve, 2000))
405
- if (this.process) {
406
- this.process.kill('SIGKILL')
407
- }
408
- }
409
- }
410
- } else {
411
- // Default: kill the process
412
- this.process.kill('SIGTERM')
413
- await new Promise(resolve => setTimeout(resolve, 2000))
414
- if (this.process) {
415
- this.process.kill('SIGKILL')
416
- }
417
- }
418
-
419
- this.process = null
420
- await this.writeLog('system', `Backend app stopped`)
421
- return { success: true }
422
- } catch (error: any) {
423
- await this.writeLog('stderr', `Error stopping backend: ${error.message}`)
424
- return {
425
- success: false,
426
- error: error.message || 'Failed to stop backend'
427
- }
428
- }
429
- }
430
-
431
- getStats(): RunnerStats {
432
- return {
433
- ...this.stats,
434
- memoryUsage: this.process ? process.memoryUsage() : undefined,
435
- }
436
- }
437
- }
438
-
439
- // ============ Runner Factory ============
440
-
441
- export function createRunner(
442
- app: ProjectApp,
443
- projectPath: string,
444
- projectId: string,
445
- callbacks: RunnerCallbacks
446
- ): BaseRunner {
447
- // Use explicit appType if provided, otherwise detect
448
- if (app.appType === 'static-frontend') {
449
- return new StaticFrontendRunner(app, projectPath, projectId, callbacks)
450
- }
451
-
452
- if (app.appType === 'backend') {
453
- return new BackendRunner(app, projectPath, projectId, callbacks)
454
- }
455
-
456
- // Auto-detect based on framework and other indicators
457
- const framework = app.framework?.toLowerCase() || ''
458
- const type = app.type?.toLowerCase() || ''
459
-
460
- // Static frontend apps
461
- if (
462
- framework.includes('react') ||
463
- framework.includes('vue') ||
464
- framework.includes('angular') ||
465
- framework.includes('svelte') ||
466
- (framework.includes('next') && type === 'http') ||
467
- app.name.toLowerCase().includes('frontend') ||
468
- app.name.toLowerCase().includes('client')
469
- ) {
470
- return new StaticFrontendRunner(app, projectPath, projectId, callbacks)
471
- }
472
-
473
- // Backend apps (Express, Fastify, etc.)
474
- return new BackendRunner(app, projectPath, projectId, callbacks)
475
- }