@exreve/exk 1.0.0

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/appManager.ts ADDED
@@ -0,0 +1,275 @@
1
+ import fs from 'fs/promises'
2
+ import path from 'path'
3
+ import os from 'os'
4
+ import type { ProjectApp } from './shared/types'
5
+ import { createRunner, BaseRunner, RunnerOutput, RunnerStats } from './appRunner'
6
+
7
+ type RunningApp = {
8
+ runner: BaseRunner
9
+ pid: number
10
+ startTime: number
11
+ logFile: string
12
+ }
13
+
14
+ // Store running app runners: projectId -> appName -> runner
15
+ const runningApps = new Map<string, Map<string, RunningApp>>()
16
+
17
+ const LOGS_DIR = path.join(os.homedir(), '.talk-to-code', 'app-logs')
18
+
19
+ /**
20
+ * Ensure logs directory exists
21
+ */
22
+ const ensureLogsDir = () => fs.mkdir(LOGS_DIR, { recursive: true })
23
+
24
+ /**
25
+ * Helper to remove app from running apps and clean up empty project maps
26
+ */
27
+ const removeAppFromRunning = (projectId: string, appName: string): void => {
28
+ const projectApps = runningApps.get(projectId)
29
+ if (projectApps) {
30
+ projectApps.delete(appName)
31
+ if (projectApps.size === 0) {
32
+ runningApps.delete(projectId)
33
+ }
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Get log file path for an app
39
+ */
40
+ function getLogFilePath(projectId: string, appName: string): string {
41
+ return path.join(LOGS_DIR, `${projectId}-${appName}.log`)
42
+ }
43
+
44
+ /**
45
+ * Start an app using wrapper runner
46
+ */
47
+ export async function startApp(
48
+ projectPath: string,
49
+ projectId: string,
50
+ app: ProjectApp
51
+ ): Promise<{ success: boolean; processId?: string; pid?: number; error?: string }> {
52
+ try {
53
+ // Check if already running
54
+ if (isAppRunning(projectId, app.name)) {
55
+ const running = getRunningApp(projectId, app.name)
56
+ return {
57
+ success: true,
58
+ processId: `${projectId}-${app.name}`,
59
+ pid: running?.pid
60
+ }
61
+ }
62
+
63
+ // Ensure logs directory exists
64
+ await ensureLogsDir()
65
+
66
+ const logFile = getLogFilePath(projectId, app.name)
67
+
68
+ // Create runner based on app type
69
+ const runner = createRunner(app, projectPath, projectId, {
70
+ onOutput: async (output: RunnerOutput) => {
71
+ // Logs are handled by the runner itself
72
+ },
73
+ onError: (error: string) => {
74
+ // Errors are logged by runner
75
+ },
76
+ onExit: (code: number | null) => {
77
+ removeAppFromRunning(projectId, app.name)
78
+ },
79
+ onStats: (stats: RunnerStats) => {
80
+ // Stats updates can be used for monitoring
81
+ },
82
+ })
83
+
84
+ // Start the runner
85
+ const result = await runner.start()
86
+
87
+ if (!result.success) {
88
+ return result
89
+ }
90
+
91
+ // Store runner info
92
+ if (!runningApps.has(projectId)) {
93
+ runningApps.set(projectId, new Map())
94
+ }
95
+ runningApps.get(projectId)!.set(app.name, {
96
+ runner,
97
+ pid: result.pid!,
98
+ startTime: Date.now(),
99
+ logFile,
100
+ })
101
+
102
+ return {
103
+ success: true,
104
+ processId: `${projectId}-${app.name}`,
105
+ pid: result.pid,
106
+ }
107
+ } catch (error: any) {
108
+ return {
109
+ success: false,
110
+ error: error.message || 'Failed to start app'
111
+ }
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Stop an app using wrapper runner
117
+ */
118
+ export async function stopApp(
119
+ projectId: string,
120
+ appName: string,
121
+ app?: ProjectApp
122
+ ): Promise<{ success: boolean; error?: string }> {
123
+ try {
124
+ const running = getRunningApp(projectId, appName)
125
+ if (!running) {
126
+ return {
127
+ success: false,
128
+ error: 'App is not running'
129
+ }
130
+ }
131
+
132
+ // Stop using runner
133
+ const result = await running.runner.stop()
134
+
135
+ if (result.success) {
136
+ removeAppFromRunning(projectId, appName)
137
+ }
138
+
139
+ return result
140
+ } catch (error: any) {
141
+ return {
142
+ success: false,
143
+ error: error.message || 'Failed to stop app'
144
+ }
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Restart an app
150
+ */
151
+ export async function restartApp(
152
+ projectPath: string,
153
+ projectId: string,
154
+ app: ProjectApp
155
+ ): Promise<{ success: boolean; processId?: string; pid?: number; error?: string }> {
156
+ // Stop first if running
157
+ if (isAppRunning(projectId, app.name)) {
158
+ const stopResult = await stopApp(projectId, app.name, app)
159
+ if (!stopResult.success) {
160
+ return stopResult
161
+ }
162
+ // Wait a bit before restarting
163
+ await new Promise(resolve => setTimeout(resolve, 1000))
164
+ }
165
+
166
+ // Start
167
+ return startApp(projectPath, projectId, app)
168
+ }
169
+
170
+ /**
171
+ * Check if an app is running
172
+ */
173
+ export function isAppRunning(projectId: string, appName: string): boolean {
174
+ const projectApps = runningApps.get(projectId)
175
+ if (!projectApps) return false
176
+
177
+ const app = projectApps.get(appName)
178
+ if (!app) return false
179
+
180
+ // Check if process is still alive
181
+ try {
182
+ process.kill(app.pid, 0) // Signal 0 just checks if process exists
183
+ return true
184
+ } catch {
185
+ // Process doesn't exist, clean up
186
+ removeAppFromRunning(projectId, appName)
187
+ return false
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Get running app info
193
+ */
194
+ export function getRunningApp(projectId: string, appName: string): RunningApp | null {
195
+ const projectApps = runningApps.get(projectId)
196
+ if (!projectApps) return null
197
+
198
+ const app = projectApps.get(appName)
199
+ if (!app) return null
200
+
201
+ // Verify process is still alive
202
+ if (!isAppRunning(projectId, appName)) {
203
+ return null
204
+ }
205
+
206
+ return app
207
+ }
208
+
209
+ /**
210
+ * Get status of all apps for a project
211
+ */
212
+ export function getAppStatuses(projectId: string, apps: ProjectApp[]): Array<{
213
+ name: string
214
+ running: boolean
215
+ processId?: string
216
+ pid?: number
217
+ }> {
218
+ return apps.map(app => {
219
+ const running = getRunningApp(projectId, app.name)
220
+ return {
221
+ name: app.name,
222
+ running: !!running,
223
+ processId: running ? `${projectId}-${app.name}` : undefined,
224
+ pid: running?.pid,
225
+ }
226
+ })
227
+ }
228
+
229
+ /**
230
+ * Get logs for an app
231
+ */
232
+ export async function getAppLogs(
233
+ projectId: string,
234
+ appName: string,
235
+ lines: number = 100
236
+ ): Promise<{ success: boolean; logs?: string; error?: string }> {
237
+ try {
238
+ const logFile = getLogFilePath(projectId, appName)
239
+
240
+ try {
241
+ await fs.access(logFile)
242
+ } catch {
243
+ return {
244
+ success: true,
245
+ logs: 'No logs available yet'
246
+ }
247
+ }
248
+
249
+ const content = await fs.readFile(logFile, 'utf-8')
250
+ const logLines = content.split('\n')
251
+ const recentLogs = logLines.slice(-lines).join('\n')
252
+
253
+ return {
254
+ success: true,
255
+ logs: recentLogs || 'No logs available'
256
+ }
257
+ } catch (error: any) {
258
+ return {
259
+ success: false,
260
+ error: error.message || 'Failed to read logs'
261
+ }
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Get stats for a running app
267
+ */
268
+ export function getAppStats(projectId: string, appName: string): RunnerStats | null {
269
+ const running = getRunningApp(projectId, appName)
270
+ if (!running) {
271
+ return null
272
+ }
273
+
274
+ return running.runner.getStats()
275
+ }