aiexecode 1.0.126 → 1.0.127
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/index.js +134 -20
- package/package.json +1 -1
- package/payload_viewer/out/404/index.html +1 -1
- package/payload_viewer/out/404.html +1 -1
- package/payload_viewer/out/index.html +1 -1
- package/payload_viewer/out/index.txt +1 -1
- package/prompts/completion_judge.txt +4 -0
- package/prompts/orchestrator.txt +53 -0
- package/src/commands/bg.js +129 -0
- package/src/frontend/App.js +100 -1
- package/src/frontend/components/BackgroundProcessList.js +175 -0
- package/src/system/ai_request.js +0 -19
- package/src/system/background_process.js +317 -0
- package/src/system/code_executer.js +469 -57
- package/src/system/output_helper.js +52 -9
- package/src/system/session.js +73 -10
- package/src/util/exit_handler.js +8 -0
- /package/payload_viewer/out/_next/static/{Ciog50_gZfMGwKNqVaI0v → 42iEoi-1o5MxNIZ1SWSvV}/_buildManifest.js +0 -0
- /package/payload_viewer/out/_next/static/{Ciog50_gZfMGwKNqVaI0v → 42iEoi-1o5MxNIZ1SWSvV}/_clientMiddlewareManifest.json +0 -0
- /package/payload_viewer/out/_next/static/{Ciog50_gZfMGwKNqVaI0v → 42iEoi-1o5MxNIZ1SWSvV}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 백그라운드 프로세스 관리 모듈
|
|
3
|
+
*
|
|
4
|
+
* AI Agent가 백그라운드로 명령어를 실행하고 관리할 수 있도록 지원
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawn } from 'child_process';
|
|
8
|
+
import { EventEmitter } from 'events';
|
|
9
|
+
import { createDebugLogger } from '../util/debug_log.js';
|
|
10
|
+
import { whichCommand } from './code_executer.js';
|
|
11
|
+
|
|
12
|
+
const debugLog = createDebugLogger('background_process.log', 'background_process');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 백그라운드 프로세스 정보
|
|
16
|
+
* @typedef {Object} BackgroundProcess
|
|
17
|
+
* @property {string} id - 고유 ID
|
|
18
|
+
* @property {string} command - 실행된 명령어
|
|
19
|
+
* @property {number} pid - 프로세스 ID
|
|
20
|
+
* @property {string} status - 상태 (running, completed, failed, killed)
|
|
21
|
+
* @property {Date} startedAt - 시작 시간
|
|
22
|
+
* @property {Date|null} endedAt - 종료 시간
|
|
23
|
+
* @property {string} stdout - 표준 출력
|
|
24
|
+
* @property {string} stderr - 표준 에러
|
|
25
|
+
* @property {number|null} exitCode - 종료 코드
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
class BackgroundProcessManager extends EventEmitter {
|
|
29
|
+
constructor() {
|
|
30
|
+
super();
|
|
31
|
+
/** @type {Map<string, BackgroundProcess>} */
|
|
32
|
+
this.processes = new Map();
|
|
33
|
+
this.nextId = 1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 고유 ID 생성
|
|
38
|
+
*/
|
|
39
|
+
_generateId() {
|
|
40
|
+
return `bg_${this.nextId++}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 백그라운드로 명령어 실행
|
|
45
|
+
* @param {string} script - 실행할 스크립트
|
|
46
|
+
* @param {Object} options - 옵션
|
|
47
|
+
* @returns {Promise<{id: string, pid: number}>}
|
|
48
|
+
*/
|
|
49
|
+
async run(script, options = {}) {
|
|
50
|
+
const shellPath = await whichCommand("bash") || await whichCommand("sh");
|
|
51
|
+
if (!shellPath) {
|
|
52
|
+
throw new Error('No shell found');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const id = this._generateId();
|
|
56
|
+
const startedAt = new Date();
|
|
57
|
+
|
|
58
|
+
debugLog(`[BackgroundProcess] Starting: id=${id}, script="${script.substring(0, 50)}..."`);
|
|
59
|
+
|
|
60
|
+
const child = spawn(shellPath, ['-c', script], {
|
|
61
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
62
|
+
env: {
|
|
63
|
+
...process.env,
|
|
64
|
+
DEBIAN_FRONTEND: 'noninteractive',
|
|
65
|
+
CI: 'true',
|
|
66
|
+
BATCH: '1',
|
|
67
|
+
},
|
|
68
|
+
cwd: options.cwd || process.cwd(),
|
|
69
|
+
detached: true,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const processInfo = {
|
|
73
|
+
id,
|
|
74
|
+
command: script,
|
|
75
|
+
pid: child.pid,
|
|
76
|
+
status: 'running',
|
|
77
|
+
startedAt,
|
|
78
|
+
endedAt: null,
|
|
79
|
+
stdout: '',
|
|
80
|
+
stderr: '',
|
|
81
|
+
exitCode: null,
|
|
82
|
+
_process: child, // 내부용
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
this.processes.set(id, processInfo);
|
|
86
|
+
|
|
87
|
+
// stdout 수집
|
|
88
|
+
child.stdout.on('data', (data) => {
|
|
89
|
+
processInfo.stdout += data.toString();
|
|
90
|
+
this.emit('output', { id, type: 'stdout', data: data.toString() });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// stderr 수집
|
|
94
|
+
child.stderr.on('data', (data) => {
|
|
95
|
+
processInfo.stderr += data.toString();
|
|
96
|
+
this.emit('output', { id, type: 'stderr', data: data.toString() });
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// 프로세스 종료 처리
|
|
100
|
+
child.on('close', (code) => {
|
|
101
|
+
processInfo.status = code === 0 ? 'completed' : 'failed';
|
|
102
|
+
processInfo.exitCode = code;
|
|
103
|
+
processInfo.endedAt = new Date();
|
|
104
|
+
delete processInfo._process;
|
|
105
|
+
|
|
106
|
+
debugLog(`[BackgroundProcess] Closed: id=${id}, code=${code}`);
|
|
107
|
+
this.emit('close', { id, code, status: processInfo.status });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
child.on('error', (err) => {
|
|
111
|
+
processInfo.status = 'failed';
|
|
112
|
+
processInfo.stderr += `\nError: ${err.message}`;
|
|
113
|
+
processInfo.endedAt = new Date();
|
|
114
|
+
delete processInfo._process;
|
|
115
|
+
|
|
116
|
+
debugLog(`[BackgroundProcess] Error: id=${id}, error=${err.message}`);
|
|
117
|
+
this.emit('error', { id, error: err });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
this.emit('started', { id, pid: child.pid, command: script });
|
|
121
|
+
|
|
122
|
+
return { id, pid: child.pid };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 프로세스 종료
|
|
127
|
+
* @param {string} id - 프로세스 ID
|
|
128
|
+
* @param {string} signal - 시그널 (기본: SIGTERM)
|
|
129
|
+
* @returns {boolean} 성공 여부
|
|
130
|
+
*/
|
|
131
|
+
kill(id, signal = 'SIGTERM') {
|
|
132
|
+
const processInfo = this.processes.get(id);
|
|
133
|
+
if (!processInfo) {
|
|
134
|
+
debugLog(`[BackgroundProcess] Kill failed: id=${id} not found`);
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (processInfo.status !== 'running') {
|
|
139
|
+
debugLog(`[BackgroundProcess] Kill skipped: id=${id} already ${processInfo.status}`);
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const child = processInfo._process;
|
|
144
|
+
if (!child) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
// 프로세스 그룹 전체 종료
|
|
150
|
+
process.kill(-child.pid, signal);
|
|
151
|
+
processInfo.status = 'killed';
|
|
152
|
+
processInfo.endedAt = new Date();
|
|
153
|
+
debugLog(`[BackgroundProcess] Killed: id=${id}, signal=${signal}`);
|
|
154
|
+
this.emit('killed', { id, signal });
|
|
155
|
+
return true;
|
|
156
|
+
} catch (err) {
|
|
157
|
+
try {
|
|
158
|
+
child.kill(signal);
|
|
159
|
+
processInfo.status = 'killed';
|
|
160
|
+
processInfo.endedAt = new Date();
|
|
161
|
+
debugLog(`[BackgroundProcess] Killed (fallback): id=${id}`);
|
|
162
|
+
this.emit('killed', { id, signal });
|
|
163
|
+
return true;
|
|
164
|
+
} catch (err2) {
|
|
165
|
+
debugLog(`[BackgroundProcess] Kill error: id=${id}, error=${err2.message}`);
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 프로세스 정보 조회
|
|
173
|
+
* @param {string} id - 프로세스 ID
|
|
174
|
+
* @returns {BackgroundProcess|null}
|
|
175
|
+
*/
|
|
176
|
+
get(id) {
|
|
177
|
+
const info = this.processes.get(id);
|
|
178
|
+
if (!info) return null;
|
|
179
|
+
|
|
180
|
+
// _process 필드 제외하고 반환
|
|
181
|
+
const { _process, ...publicInfo } = info;
|
|
182
|
+
return publicInfo;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 모든 프로세스 목록 조회
|
|
187
|
+
* @param {Object} filter - 필터 옵션
|
|
188
|
+
* @returns {BackgroundProcess[]}
|
|
189
|
+
*/
|
|
190
|
+
list(filter = {}) {
|
|
191
|
+
let result = [];
|
|
192
|
+
|
|
193
|
+
for (const [id, info] of this.processes) {
|
|
194
|
+
const { _process, ...publicInfo } = info;
|
|
195
|
+
|
|
196
|
+
// 필터 적용
|
|
197
|
+
if (filter.status && publicInfo.status !== filter.status) continue;
|
|
198
|
+
if (filter.running && publicInfo.status !== 'running') continue;
|
|
199
|
+
|
|
200
|
+
result.push(publicInfo);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 시작 시간순 정렬
|
|
204
|
+
result.sort((a, b) => b.startedAt - a.startedAt);
|
|
205
|
+
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* 실행 중인 프로세스 수
|
|
211
|
+
*/
|
|
212
|
+
get runningCount() {
|
|
213
|
+
let count = 0;
|
|
214
|
+
for (const info of this.processes.values()) {
|
|
215
|
+
if (info.status === 'running') count++;
|
|
216
|
+
}
|
|
217
|
+
return count;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* 완료된 프로세스 정리
|
|
222
|
+
* @param {number} maxAge - 최대 보관 시간 (ms)
|
|
223
|
+
*/
|
|
224
|
+
cleanup(maxAge = 3600000) {
|
|
225
|
+
const now = Date.now();
|
|
226
|
+
const toDelete = [];
|
|
227
|
+
|
|
228
|
+
for (const [id, info] of this.processes) {
|
|
229
|
+
if (info.status !== 'running' && info.endedAt) {
|
|
230
|
+
if (now - info.endedAt.getTime() > maxAge) {
|
|
231
|
+
toDelete.push(id);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
for (const id of toDelete) {
|
|
237
|
+
this.processes.delete(id);
|
|
238
|
+
debugLog(`[BackgroundProcess] Cleaned up: id=${id}`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return toDelete.length;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* 모든 실행 중인 프로세스 종료
|
|
246
|
+
*/
|
|
247
|
+
async killAll() {
|
|
248
|
+
const killed = [];
|
|
249
|
+
for (const [id, info] of this.processes) {
|
|
250
|
+
if (info.status === 'running') {
|
|
251
|
+
if (this.kill(id)) {
|
|
252
|
+
killed.push(id);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return killed;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 싱글톤 인스턴스
|
|
261
|
+
let instance = null;
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* BackgroundProcessManager 싱글톤 인스턴스 가져오기
|
|
265
|
+
*/
|
|
266
|
+
export function getBackgroundProcessManager() {
|
|
267
|
+
if (!instance) {
|
|
268
|
+
instance = new BackgroundProcessManager();
|
|
269
|
+
}
|
|
270
|
+
return instance;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* 백그라운드로 명령어 실행
|
|
275
|
+
* @param {string} script - 실행할 스크립트
|
|
276
|
+
* @param {Object} options - 옵션
|
|
277
|
+
* @returns {Promise<{id: string, pid: number}>}
|
|
278
|
+
*/
|
|
279
|
+
export async function runInBackground(script, options = {}) {
|
|
280
|
+
const manager = getBackgroundProcessManager();
|
|
281
|
+
return await manager.run(script, options);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* 백그라운드 프로세스 종료
|
|
286
|
+
* @param {string} id - 프로세스 ID
|
|
287
|
+
*/
|
|
288
|
+
export function killBackgroundProcess(id) {
|
|
289
|
+
const manager = getBackgroundProcessManager();
|
|
290
|
+
return manager.kill(id);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* 백그라운드 프로세스 목록 조회
|
|
295
|
+
*/
|
|
296
|
+
export function listBackgroundProcesses(filter = {}) {
|
|
297
|
+
const manager = getBackgroundProcessManager();
|
|
298
|
+
return manager.list(filter);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* 백그라운드 프로세스 정보 조회
|
|
303
|
+
*/
|
|
304
|
+
export function getBackgroundProcess(id) {
|
|
305
|
+
const manager = getBackgroundProcessManager();
|
|
306
|
+
return manager.get(id);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* 모든 백그라운드 프로세스 종료 (앱 종료 시)
|
|
311
|
+
*/
|
|
312
|
+
export async function killAllBackgroundProcesses() {
|
|
313
|
+
const manager = getBackgroundProcessManager();
|
|
314
|
+
return await manager.killAll();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export { BackgroundProcessManager };
|