@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/README.md +109 -0
- package/agentLogger.ts +162 -0
- package/agentSession.ts +1176 -0
- package/app-child.ts +2769 -0
- package/appManager.ts +275 -0
- package/appRunner.ts +475 -0
- package/bin/exk +45 -0
- package/container-entrypoint.sh +177 -0
- package/index.ts +2798 -0
- package/install-service.sh +122 -0
- package/moduleMcpServer.ts +131 -0
- package/package.json +67 -0
- package/projectAnalyzer.ts +341 -0
- package/projectManager.ts +111 -0
- package/runnerGenerator.ts +218 -0
- package/shared/types.ts +488 -0
- package/skills/code-review.md +49 -0
- package/skills/front-glass.md +36 -0
- package/skills/frontend-design.md +41 -0
- package/skills/index.ts +151 -0
- package/tsconfig.json +22 -0
- package/updater.ts +512 -0
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
|
+
}
|