@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
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import fs from 'fs/promises'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
|
|
4
|
+
const toAbsolutePath = (p: string) => path.isAbsolute(p) ? p : path.resolve(p)
|
|
5
|
+
|
|
6
|
+
interface ProjectCreateRequest {
|
|
7
|
+
projectId: string
|
|
8
|
+
name: string
|
|
9
|
+
path: string
|
|
10
|
+
sourcePath?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ProjectInfo {
|
|
14
|
+
projectId: string
|
|
15
|
+
name: string
|
|
16
|
+
path: string
|
|
17
|
+
sourcePath?: string
|
|
18
|
+
exists: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function createProject(request: ProjectCreateRequest): Promise<{ success: boolean; error?: string; actualPath?: string }> {
|
|
22
|
+
try {
|
|
23
|
+
const { projectId, name, path: projectPath, sourcePath } = request
|
|
24
|
+
const absolutePath = toAbsolutePath(projectPath)
|
|
25
|
+
|
|
26
|
+
// Check if source path exists (if linking)
|
|
27
|
+
if (sourcePath) {
|
|
28
|
+
const absoluteSourcePath = toAbsolutePath(sourcePath)
|
|
29
|
+
try {
|
|
30
|
+
const sourceStat = await fs.stat(absoluteSourcePath)
|
|
31
|
+
if (!sourceStat.isDirectory()) {
|
|
32
|
+
return { success: false, error: 'Source path is not a directory' }
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
return { success: false, error: 'Source path does not exist' }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Check if project directory already exists
|
|
40
|
+
try {
|
|
41
|
+
const stat = await fs.stat(absolutePath)
|
|
42
|
+
if (!stat.isDirectory()) {
|
|
43
|
+
return { success: false, error: 'Path exists but is not a directory' }
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
// Directory doesn't exist, create it
|
|
47
|
+
try {
|
|
48
|
+
await fs.mkdir(absolutePath, { recursive: true })
|
|
49
|
+
} catch (error: any) {
|
|
50
|
+
return { success: false, error: `Failed to create directory: ${error.message}` }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// If linking to source, create symlink or copy (for now, just validate)
|
|
55
|
+
// In a real implementation, you might want to create a symlink or copy files
|
|
56
|
+
if (sourcePath) {
|
|
57
|
+
// For now, we just validate that both paths exist
|
|
58
|
+
// The actual linking can be done by the application logic
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { success: true, actualPath: absolutePath }
|
|
62
|
+
} catch (error: any) {
|
|
63
|
+
return { success: false, error: error.message }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function deleteProject(projectPath: string): Promise<{ success: boolean; error?: string }> {
|
|
68
|
+
try {
|
|
69
|
+
const absolutePath = toAbsolutePath(projectPath)
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const stat = await fs.stat(absolutePath)
|
|
73
|
+
if (!stat.isDirectory()) {
|
|
74
|
+
return { success: false, error: 'Path is not a directory' }
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
// Directory doesn't exist, consider it deleted
|
|
78
|
+
return { success: true }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Don't delete the directory on device - just succeed
|
|
82
|
+
// The directory will remain on the filesystem
|
|
83
|
+
return { success: true }
|
|
84
|
+
} catch (error: any) {
|
|
85
|
+
return { success: false, error: error.message }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function getProjectInfo(projectPath: string): Promise<ProjectInfo | null> {
|
|
90
|
+
try {
|
|
91
|
+
const absolutePath = toAbsolutePath(projectPath)
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const stat = await fs.stat(absolutePath)
|
|
95
|
+
if (!stat.isDirectory()) {
|
|
96
|
+
return null
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
projectId: '', // Will be set by caller
|
|
101
|
+
name: path.basename(absolutePath),
|
|
102
|
+
path: absolutePath,
|
|
103
|
+
exists: true
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
return null
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import type { ProjectApp } from './shared/types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate TypeScript runner code for an app
|
|
5
|
+
*/
|
|
6
|
+
export function generateRunnerCode(app: ProjectApp, projectPath: string): string {
|
|
7
|
+
const appName = app.name.replace(/[^a-zA-Z0-9_-]/g, '_') // Sanitize app name for filename
|
|
8
|
+
const appType = app.appType || (app.framework?.toLowerCase().includes('react') ||
|
|
9
|
+
app.framework?.toLowerCase().includes('vue') ||
|
|
10
|
+
app.framework?.toLowerCase().includes('angular') ||
|
|
11
|
+
app.framework?.toLowerCase().includes('svelte') ? 'static-frontend' : 'backend')
|
|
12
|
+
|
|
13
|
+
if (appType === 'static-frontend') {
|
|
14
|
+
return generateStaticFrontendRunner(app, projectPath)
|
|
15
|
+
} else {
|
|
16
|
+
return generateBackendRunner(app, projectPath)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function generateStaticFrontendRunner(app: ProjectApp, projectPath: string): string {
|
|
21
|
+
const appName = app.name.replace(/[^a-zA-Z0-9_-]/g, '_') // Sanitize app name for filename
|
|
22
|
+
const port = app.port || 3000
|
|
23
|
+
const buildDir = app.buildDir || 'dist'
|
|
24
|
+
const appDir = app.directory || ''
|
|
25
|
+
|
|
26
|
+
return `import Fastify from 'fastify'
|
|
27
|
+
import fastifyStatic from '@fastify/static'
|
|
28
|
+
import path from 'path'
|
|
29
|
+
import fs from 'fs/promises'
|
|
30
|
+
import { fileURLToPath } from 'url'
|
|
31
|
+
import { dirname } from 'path'
|
|
32
|
+
|
|
33
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
34
|
+
const __dirname = dirname(__filename)
|
|
35
|
+
const PORT = ${port}
|
|
36
|
+
const BUILD_DIR = path.join(__dirname, ${appDir ? `'${appDir}', ` : ''}'${buildDir}')
|
|
37
|
+
|
|
38
|
+
async function start() {
|
|
39
|
+
// Check if build directory exists
|
|
40
|
+
try {
|
|
41
|
+
await fs.access(BUILD_DIR)
|
|
42
|
+
} catch {
|
|
43
|
+
console.error(\`✗ Build directory not found: \${BUILD_DIR}\`)
|
|
44
|
+
console.error(' Please build the project first using: ' + (process.env.BUILD_COMMAND || 'npm run build'))
|
|
45
|
+
process.exit(1)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const fastify = Fastify({
|
|
49
|
+
logger: {
|
|
50
|
+
level: 'info',
|
|
51
|
+
transport: {
|
|
52
|
+
target: 'pino-pretty',
|
|
53
|
+
options: {
|
|
54
|
+
translateTime: 'HH:MM:ss Z',
|
|
55
|
+
ignore: 'pid,hostname',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// Register static file serving
|
|
62
|
+
await fastify.register(fastifyStatic, {
|
|
63
|
+
root: BUILD_DIR,
|
|
64
|
+
prefix: '/',
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
// Request logging
|
|
68
|
+
fastify.addHook('onRequest', async (request, reply) => {
|
|
69
|
+
const logLine = \`[\${new Date().toISOString()}] \${request.method} \${request.url} - \${reply.statusCode}\\n\`
|
|
70
|
+
await fs.appendFile('${appName}_runner.log', logLine, 'utf-8').catch(() => {})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// Error handling
|
|
74
|
+
fastify.setErrorHandler(async (error, request, reply) => {
|
|
75
|
+
const logLine = \`[\${new Date().toISOString()}] ERROR: \${error.message} - \${request.method} \${request.url}\\n\`
|
|
76
|
+
await fs.appendFile('${appName}_runner.log', logLine, 'utf-8').catch(() => {})
|
|
77
|
+
reply.status(500).send({ error: 'Internal Server Error' })
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
await fastify.listen({ port: PORT, host: '0.0.0.0' })
|
|
82
|
+
console.log(\`✓ Static server started on port \${PORT}\`)
|
|
83
|
+
console.log(\` Serving from: \${BUILD_DIR}\`)
|
|
84
|
+
} catch (error: any) {
|
|
85
|
+
console.error(\`✗ Failed to start server:\`, error.message)
|
|
86
|
+
process.exit(1)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Handle graceful shutdown
|
|
91
|
+
process.on('SIGTERM', async () => {
|
|
92
|
+
console.log('Shutting down...')
|
|
93
|
+
await fastify.close()
|
|
94
|
+
process.exit(0)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
process.on('SIGINT', async () => {
|
|
98
|
+
console.log('Shutting down...')
|
|
99
|
+
await fastify.close()
|
|
100
|
+
process.exit(0)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
start()
|
|
104
|
+
`
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function generateBackendRunner(app: ProjectApp, projectPath: string): string {
|
|
108
|
+
const appName = app.name.replace(/[^a-zA-Z0-9_-]/g, '_') // Sanitize app name for filename
|
|
109
|
+
const appDir = app.directory || ''
|
|
110
|
+
const startCommand = app.startCommand
|
|
111
|
+
const envVars = app.env || {}
|
|
112
|
+
|
|
113
|
+
const envString = Object.entries(envVars)
|
|
114
|
+
.map(([key, value]) => ` ${key}: '${value}',`)
|
|
115
|
+
.join('\n')
|
|
116
|
+
|
|
117
|
+
return `import { spawn } from 'child_process'
|
|
118
|
+
import fs from 'fs/promises'
|
|
119
|
+
import path from 'path'
|
|
120
|
+
import { fileURLToPath } from 'url'
|
|
121
|
+
import { dirname } from 'path'
|
|
122
|
+
|
|
123
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
124
|
+
const __dirname = dirname(__filename)
|
|
125
|
+
const APP_DIR = path.join(__dirname, ${appDir ? `'${appDir}'` : 'undefined'})
|
|
126
|
+
const START_COMMAND = '${startCommand}'
|
|
127
|
+
|
|
128
|
+
const env = {
|
|
129
|
+
...process.env,
|
|
130
|
+
${envString}
|
|
131
|
+
NODE_ENV: process.env.NODE_ENV || 'development',
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let childProcess: ReturnType<typeof spawn> | null = null
|
|
135
|
+
|
|
136
|
+
async function start() {
|
|
137
|
+
const workingDir = APP_DIR || process.cwd()
|
|
138
|
+
|
|
139
|
+
console.log(\`Starting backend app: ${app.name}\`)
|
|
140
|
+
console.log(\` Directory: \${workingDir}\`)
|
|
141
|
+
console.log(\` Command: \${START_COMMAND}\`)
|
|
142
|
+
|
|
143
|
+
const isWin = process.platform === 'win32'
|
|
144
|
+
const shell = isWin ? (process.env.COMSPEC || 'cmd.exe') : 'sh'
|
|
145
|
+
const args = isWin ? ['/c', START_COMMAND] : ['-c', START_COMMAND]
|
|
146
|
+
childProcess = spawn(shell, args, {
|
|
147
|
+
cwd: workingDir,
|
|
148
|
+
env,
|
|
149
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
if (!childProcess.pid) {
|
|
153
|
+
console.error('✗ Failed to spawn process')
|
|
154
|
+
process.exit(1)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.log(\`✓ Backend started with PID: \${childProcess.pid}\`)
|
|
158
|
+
|
|
159
|
+
// Capture stdout
|
|
160
|
+
childProcess.stdout?.on('data', async (data: Buffer) => {
|
|
161
|
+
const output = data.toString()
|
|
162
|
+
const logLine = \`[\${new Date().toISOString()}] [STDOUT] \${output}\`
|
|
163
|
+
await fs.appendFile('${appName}_runner.log', logLine, 'utf-8').catch(() => {})
|
|
164
|
+
process.stdout.write(output)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
// Capture stderr
|
|
168
|
+
childProcess.stderr?.on('data', async (data: Buffer) => {
|
|
169
|
+
const output = data.toString()
|
|
170
|
+
const logLine = \`[\${new Date().toISOString()}] [STDERR] \${output}\`
|
|
171
|
+
await fs.appendFile('${appName}_runner.log', logLine, 'utf-8').catch(() => {})
|
|
172
|
+
process.stderr.write(output)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
// Handle process exit
|
|
176
|
+
childProcess.on('exit', async (code) => {
|
|
177
|
+
const logLine = \`[\${new Date().toISOString()}] Process exited with code \${code}\\n\`
|
|
178
|
+
await fs.appendFile('${appName}_runner.log', logLine, 'utf-8').catch(() => {})
|
|
179
|
+
console.log(\`Process exited with code \${code}\`)
|
|
180
|
+
childProcess = null
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
// Handle process errors
|
|
184
|
+
childProcess.on('error', async (error) => {
|
|
185
|
+
const logLine = \`[\${new Date().toISOString()}] Process error: \${error.message}\\n\`
|
|
186
|
+
await fs.appendFile('${appName}_runner.log', logLine, 'utf-8').catch(() => {})
|
|
187
|
+
console.error(\`Process error: \${error.message}\`)
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Handle graceful shutdown
|
|
192
|
+
process.on('SIGTERM', async () => {
|
|
193
|
+
console.log('Shutting down...')
|
|
194
|
+
if (childProcess) {
|
|
195
|
+
childProcess.kill('SIGTERM')
|
|
196
|
+
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
197
|
+
if (childProcess) {
|
|
198
|
+
childProcess.kill('SIGKILL')
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
process.exit(0)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
process.on('SIGINT', async () => {
|
|
205
|
+
console.log('Shutting down...')
|
|
206
|
+
if (childProcess) {
|
|
207
|
+
childProcess.kill('SIGTERM')
|
|
208
|
+
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
209
|
+
if (childProcess) {
|
|
210
|
+
childProcess.kill('SIGKILL')
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
process.exit(0)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
start()
|
|
217
|
+
`
|
|
218
|
+
}
|