aiexecode 1.0.111 → 1.0.113
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.
Potentially problematic release.
This version of aiexecode might be problematic. Click here for more details.
- package/index.js +135 -59
- package/mcp-agent-lib/src/mcp_message_logger.js +17 -16
- 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/orchestrator.txt +131 -3
- package/src/ai_based/orchestrator.js +27 -2
- package/src/config/feature_flags.js +6 -7
- package/src/system/code_executer.js +30 -2
- package/src/system/conversation_trimmer.js +132 -0
- package/src/system/file_integrity.js +57 -0
- package/src/system/session.js +82 -14
- package/src/system/session_memory.js +30 -2
- package/src/system/system_info.js +254 -40
- package/src/tools/file_reader.js +17 -0
- package/src/util/prompt_loader.js +23 -0
- /package/payload_viewer/out/_next/static/{BqLAmXZiz76q8SE-Oia_y → CRVqYR5xcqEY3rgzrCh4K}/_buildManifest.js +0 -0
- /package/payload_viewer/out/_next/static/{BqLAmXZiz76q8SE-Oia_y → CRVqYR5xcqEY3rgzrCh4K}/_clientMiddlewareManifest.json +0 -0
- /package/payload_viewer/out/_next/static/{BqLAmXZiz76q8SE-Oia_y → CRVqYR5xcqEY3rgzrCh4K}/_ssgManifest.js +0 -0
package/src/system/session.js
CHANGED
|
@@ -12,7 +12,7 @@ import { TODO_WRITE_FUNCTIONS } from '../tools/todo_write.js';
|
|
|
12
12
|
import { SKILL_FUNCTIONS } from '../tools/skill_tool.js';
|
|
13
13
|
import { clampOutput, formatToolStdout } from "../util/output_formatter.js";
|
|
14
14
|
import { buildToolHistoryEntry } from "../util/rag_helper.js";
|
|
15
|
-
import { createSessionData, getLastConversationState, loadPreviousSessions, saveSessionToHistory, saveTodosToSession, restoreTodosFromSession, updateCurrentTodos, getCurrentTodos } from "./session_memory.js";
|
|
15
|
+
import { createSessionData, getLastConversationState, loadPreviousSessions, saveSessionToHistory, saveTodosToSession, restoreTodosFromSession, saveTrimmedFileReadsToSession, restoreTrimmedFileReadsFromSession, updateCurrentTodos, getCurrentTodos } from "./session_memory.js";
|
|
16
16
|
import { uiEvents } from "./ui_events.js";
|
|
17
17
|
import { logSystem, logError, logAssistantMessage, logToolCall, logToolResult, logCodeExecution, logCodeResult, logIteration, logMissionComplete, logConversationRestored } from "./output_helper.js";
|
|
18
18
|
import { requiresApproval, requestApproval } from "./tool_approval.js";
|
|
@@ -21,7 +21,7 @@ import { abortCurrentRequest } from "./ai_request.js";
|
|
|
21
21
|
import { safeReadFile } from '../util/safe_fs.js';
|
|
22
22
|
import { resolve } from 'path';
|
|
23
23
|
import { createDebugLogger } from '../util/debug_log.js';
|
|
24
|
-
import {
|
|
24
|
+
import { loadSettings } from '../util/config.js';
|
|
25
25
|
import {
|
|
26
26
|
MAX_REASONING_ONLY_RESPONSES,
|
|
27
27
|
DEFAULT_MAX_ITERATIONS,
|
|
@@ -825,6 +825,9 @@ export async function runSession(options) {
|
|
|
825
825
|
// 세션 시작 알림
|
|
826
826
|
uiEvents.sessionStart('Running agent session...');
|
|
827
827
|
|
|
828
|
+
// catch 블록에서도 접근 가능하도록 try 블록 밖에서 선언
|
|
829
|
+
let currentSessionData = null;
|
|
830
|
+
|
|
828
831
|
try {
|
|
829
832
|
// 현재 세션 데이터 생성
|
|
830
833
|
const {
|
|
@@ -841,7 +844,7 @@ export async function runSession(options) {
|
|
|
841
844
|
// previousSessions가 제공되지 않은 경우 자동으로 로드
|
|
842
845
|
const sessionsToUse = previousSessions ?? await loadPreviousSessions(process.app_custom.sessionID);
|
|
843
846
|
|
|
844
|
-
|
|
847
|
+
currentSessionData = createSessionData(process.app_custom.sessionID, mission);
|
|
845
848
|
|
|
846
849
|
// 파일 무결성 시스템에 현재 세션 ID 설정
|
|
847
850
|
setCurrentSession(process.app_custom.sessionID);
|
|
@@ -887,6 +890,11 @@ export async function runSession(options) {
|
|
|
887
890
|
restoreTodosFromSession({ currentTodos: conversationState.currentTodos });
|
|
888
891
|
debugLog(`[runSession] Restored ${conversationState.currentTodos.length} todos from previous session`);
|
|
889
892
|
}
|
|
893
|
+
// TrimmedFileReads 복원
|
|
894
|
+
if (conversationState.trimmedFileReads) {
|
|
895
|
+
restoreTrimmedFileReadsFromSession({ trimmedFileReads: conversationState.trimmedFileReads });
|
|
896
|
+
debugLog(`[runSession] Restored ${conversationState.trimmedFileReads.length} trimmed file reads from previous session`);
|
|
897
|
+
}
|
|
890
898
|
// logSuccess('✓ Conversation state restored');
|
|
891
899
|
} else {
|
|
892
900
|
// logSystem('ℹ Starting with fresh conversation state');
|
|
@@ -1054,6 +1062,9 @@ export async function runSession(options) {
|
|
|
1054
1062
|
// Todos를 세션에 저장
|
|
1055
1063
|
saveTodosToSession(currentSessionData);
|
|
1056
1064
|
|
|
1065
|
+
// TrimmedFileReads를 세션에 저장
|
|
1066
|
+
saveTrimmedFileReadsToSession(currentSessionData);
|
|
1067
|
+
|
|
1057
1068
|
debugLog(`[ITERATION ${iteration_count}] Saving session to history after completion_judge (mission_solved=${mission_solved})`);
|
|
1058
1069
|
await saveSessionToHistory(currentSessionData).catch(err => {
|
|
1059
1070
|
debugLog(`[ITERATION ${iteration_count}] Failed to save session after completion_judge: ${err.message}`);
|
|
@@ -1082,10 +1093,14 @@ export async function runSession(options) {
|
|
|
1082
1093
|
currentSessionData.orchestratorConversation = getOrchestratorConversation();
|
|
1083
1094
|
currentSessionData.orchestratorRequestOptions = null;
|
|
1084
1095
|
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1096
|
+
// 인터럽트 시에도 Todos와 TrimmedFileReads 저장 (다음 세션에서 복원 가능하도록)
|
|
1097
|
+
saveTodosToSession(currentSessionData);
|
|
1098
|
+
saveTrimmedFileReadsToSession(currentSessionData);
|
|
1099
|
+
|
|
1100
|
+
debugLog(`[ITERATION ${iteration_count}] Session interrupted - saving trimmedFileReads and todos`);
|
|
1101
|
+
await saveSessionToHistory(currentSessionData).catch(err => {
|
|
1102
|
+
debugLog(`[ITERATION ${iteration_count}] Failed to save session on interrupt: ${err.message}`);
|
|
1103
|
+
});
|
|
1089
1104
|
break;
|
|
1090
1105
|
}
|
|
1091
1106
|
|
|
@@ -1119,6 +1134,21 @@ export async function runSession(options) {
|
|
|
1119
1134
|
: '✅ No function calls detected - mission complete';
|
|
1120
1135
|
uiEvents.addSystemMessage(message);
|
|
1121
1136
|
debugLog(`[ITERATION ${iteration_count}] ${message}`);
|
|
1137
|
+
|
|
1138
|
+
// completion_judge 없이 종료되는 경우에도 Todos와 TrimmedFileReads 저장
|
|
1139
|
+
currentSessionData.completed_at = new Date().toISOString();
|
|
1140
|
+
currentSessionData.mission_solved = true;
|
|
1141
|
+
currentSessionData.iteration_count = iteration_count;
|
|
1142
|
+
currentSessionData.toolUsageHistory = toolUsageHistory;
|
|
1143
|
+
currentSessionData.orchestratorConversation = getOrchestratorConversation();
|
|
1144
|
+
currentSessionData.orchestratorRequestOptions = null;
|
|
1145
|
+
saveTodosToSession(currentSessionData);
|
|
1146
|
+
saveTrimmedFileReadsToSession(currentSessionData);
|
|
1147
|
+
debugLog(`[ITERATION ${iteration_count}] Saving session on no-function-call exit`);
|
|
1148
|
+
await saveSessionToHistory(currentSessionData).catch(err => {
|
|
1149
|
+
debugLog(`[ITERATION ${iteration_count}] Failed to save session: ${err.message}`);
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1122
1152
|
debugLog('========================================');
|
|
1123
1153
|
debugLog(`========== ITERATION ${iteration_count} END ==========`);
|
|
1124
1154
|
debugLog('========================================');
|
|
@@ -1158,12 +1188,17 @@ export async function runSession(options) {
|
|
|
1158
1188
|
currentSessionData.orchestratorConversation = result.orchestratorConversation;
|
|
1159
1189
|
currentSessionData.orchestratorRequestOptions = null; // 필요시 orchestrator에서 가져올 수 있음
|
|
1160
1190
|
|
|
1161
|
-
|
|
1191
|
+
// 루프 정상 종료 시에도 Todos와 TrimmedFileReads 저장
|
|
1192
|
+
saveTodosToSession(currentSessionData);
|
|
1193
|
+
saveTrimmedFileReadsToSession(currentSessionData);
|
|
1194
|
+
|
|
1195
|
+
debugLog(`[runSession] Saving final session to history - sessionID: ${currentSessionData.sessionID}`);
|
|
1162
1196
|
debugLog(`[runSession] Session data size: ${JSON.stringify(currentSessionData).length} bytes`);
|
|
1163
1197
|
debugLog(`[runSession] Final state - mission_solved: ${currentSessionData.mission_solved}, iteration_count: ${currentSessionData.iteration_count}`);
|
|
1164
1198
|
debugLog(`[runSession] Tool usage history entries: ${currentSessionData.toolUsageHistory.length}`);
|
|
1165
|
-
|
|
1166
|
-
|
|
1199
|
+
await saveSessionToHistory(currentSessionData).catch(err => {
|
|
1200
|
+
debugLog(`[runSession] Failed to save final session: ${err.message}`);
|
|
1201
|
+
});
|
|
1167
1202
|
|
|
1168
1203
|
debugLog('========================================');
|
|
1169
1204
|
debugLog('========== runSession END ==========');
|
|
@@ -1179,9 +1214,13 @@ export async function runSession(options) {
|
|
|
1179
1214
|
// 에러 메시지를 history에 표시 (한 번에 통합)
|
|
1180
1215
|
const detailMessage = error?.error?.message || errorMessage;
|
|
1181
1216
|
|
|
1217
|
+
// debug 설정 확인 (SHOW_API_PAYLOAD가 true면 verbose 모드)
|
|
1218
|
+
const settings = await loadSettings().catch(() => ({}));
|
|
1219
|
+
const isDebugMode = settings?.SHOW_API_PAYLOAD === true;
|
|
1220
|
+
|
|
1182
1221
|
let consolidatedErrorMessage;
|
|
1183
|
-
if (
|
|
1184
|
-
// 상세
|
|
1222
|
+
if (isDebugMode) {
|
|
1223
|
+
// 상세 모드 (/debug on): 모든 에러 정보 표시
|
|
1185
1224
|
consolidatedErrorMessage = [
|
|
1186
1225
|
`[Session] Internal session error: ${errorType}`,
|
|
1187
1226
|
` ├─ Message: ${detailMessage}`,
|
|
@@ -1192,12 +1231,41 @@ export async function runSession(options) {
|
|
|
1192
1231
|
` └─ Stack trace: ${errorStack}`
|
|
1193
1232
|
].join('\n');
|
|
1194
1233
|
} else {
|
|
1195
|
-
// 간결 모드 (
|
|
1196
|
-
|
|
1234
|
+
// 간결 모드 (기본값, /debug off): 사용자 친화적 에러 메시지
|
|
1235
|
+
const statusNum = parseInt(errorStatus) || parseInt(error?.status);
|
|
1236
|
+
let userFriendlyMessage;
|
|
1237
|
+
|
|
1238
|
+
if (statusNum === 503 || detailMessage.includes('503')) {
|
|
1239
|
+
userFriendlyMessage = 'AI 서버가 일시적으로 응답하지 않습니다. 잠시 후 다시 시도해주세요.';
|
|
1240
|
+
} else if (statusNum === 500 || detailMessage.includes('500')) {
|
|
1241
|
+
userFriendlyMessage = 'AI 서버에서 오류가 발생했습니다. 잠시 후 다시 시도해주세요.';
|
|
1242
|
+
} else if (statusNum === 401 || errorCode === 'invalid_api_key') {
|
|
1243
|
+
userFriendlyMessage = 'API 키가 유효하지 않습니다. 설정을 확인해주세요.';
|
|
1244
|
+
} else if (statusNum === 429) {
|
|
1245
|
+
userFriendlyMessage = '요청 한도를 초과했습니다. 잠시 후 다시 시도해주세요.';
|
|
1246
|
+
} else if (errorType === 'AbortError' || errorMessage.includes('aborted')) {
|
|
1247
|
+
userFriendlyMessage = '요청이 취소되었습니다.';
|
|
1248
|
+
} else {
|
|
1249
|
+
userFriendlyMessage = '오류가 발생했습니다. 문제가 지속되면 /debug on 으로 상세 정보를 확인하세요.';
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
consolidatedErrorMessage = `[Session] ${userFriendlyMessage}`;
|
|
1197
1253
|
}
|
|
1198
1254
|
|
|
1199
1255
|
uiEvents.addErrorMessage(consolidatedErrorMessage);
|
|
1200
1256
|
|
|
1257
|
+
// 에러 발생 시에도 Todos와 TrimmedFileReads 저장 시도
|
|
1258
|
+
if (currentSessionData) {
|
|
1259
|
+
currentSessionData.completed_at = new Date().toISOString();
|
|
1260
|
+
currentSessionData.mission_solved = false;
|
|
1261
|
+
saveTodosToSession(currentSessionData);
|
|
1262
|
+
saveTrimmedFileReadsToSession(currentSessionData);
|
|
1263
|
+
debugLog(`[runSession] Saving session on error`);
|
|
1264
|
+
await saveSessionToHistory(currentSessionData).catch(err => {
|
|
1265
|
+
debugLog(`[runSession] Failed to save session on error: ${err.message}`);
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1201
1269
|
// 에러를 throw하지 않고 정상적으로 종료
|
|
1202
1270
|
return null;
|
|
1203
1271
|
} finally {
|
|
@@ -4,6 +4,7 @@ import { join, resolve } from 'path';
|
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import { formatToolCall, formatToolResult, getToolDisplayName, getToolDisplayConfig } from './tool_registry.js';
|
|
6
6
|
import { createDebugLogger } from '../util/debug_log.js';
|
|
7
|
+
import { getTrimmedFileReads, setTrimmedFileReads } from './conversation_trimmer.js';
|
|
7
8
|
|
|
8
9
|
const MAX_HISTORY_SESSIONS = 1; // 최대 보관 세션 수
|
|
9
10
|
|
|
@@ -11,7 +12,7 @@ const debugLog = createDebugLogger('session_memory.log', 'session_memory');
|
|
|
11
12
|
|
|
12
13
|
// 현재 작업 디렉토리 기준 세션 디렉토리 경로 생성
|
|
13
14
|
function getSessionDir(sessionID) {
|
|
14
|
-
return join(process.cwd(), '.aiexe', sessionID);
|
|
15
|
+
return join(process.cwd(), '.aiexe', 'sessions', sessionID);
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
// 세션 히스토리 파일 경로 생성
|
|
@@ -150,7 +151,8 @@ export function getLastConversationState(sessions) {
|
|
|
150
151
|
return {
|
|
151
152
|
orchestratorConversation: lastSession.orchestratorConversation || [],
|
|
152
153
|
orchestratorRequestOptions: lastSession.orchestratorRequestOptions || null,
|
|
153
|
-
currentTodos: lastSession.currentTodos || []
|
|
154
|
+
currentTodos: lastSession.currentTodos || [],
|
|
155
|
+
trimmedFileReads: lastSession.trimmedFileReads || []
|
|
154
156
|
};
|
|
155
157
|
}
|
|
156
158
|
|
|
@@ -450,3 +452,29 @@ export function restoreTodosFromSession(sessionData) {
|
|
|
450
452
|
currentSessionTodos = [];
|
|
451
453
|
}
|
|
452
454
|
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* 세션 데이터에 trimmedFileReads를 저장
|
|
458
|
+
* @param {Object} sessionData - 세션 데이터 객체
|
|
459
|
+
*/
|
|
460
|
+
export function saveTrimmedFileReadsToSession(sessionData) {
|
|
461
|
+
debugLog('========== saveTrimmedFileReadsToSession ==========');
|
|
462
|
+
const trimmedFiles = getTrimmedFileReads();
|
|
463
|
+
debugLog(`Saving ${trimmedFiles.length} trimmed file reads to session`);
|
|
464
|
+
sessionData.trimmedFileReads = trimmedFiles;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* 세션 데이터에서 trimmedFileReads를 복원
|
|
469
|
+
* @param {Object} sessionData - 세션 데이터 객체
|
|
470
|
+
*/
|
|
471
|
+
export function restoreTrimmedFileReadsFromSession(sessionData) {
|
|
472
|
+
debugLog('========== restoreTrimmedFileReadsFromSession ==========');
|
|
473
|
+
if (sessionData && Array.isArray(sessionData.trimmedFileReads)) {
|
|
474
|
+
debugLog(`Restoring ${sessionData.trimmedFileReads.length} trimmed file reads from session`);
|
|
475
|
+
setTrimmedFileReads(sessionData.trimmedFileReads);
|
|
476
|
+
} else {
|
|
477
|
+
debugLog('No trimmed file reads to restore');
|
|
478
|
+
setTrimmedFileReads([]);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { exec } from "child_process";
|
|
2
2
|
import { promisify } from "util";
|
|
3
3
|
import { platform } from "os";
|
|
4
|
+
import { readFileSync, existsSync } from "fs";
|
|
4
5
|
|
|
5
6
|
const execAsync = promisify(exec);
|
|
6
7
|
|
|
@@ -23,6 +24,95 @@ function getOSType() {
|
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Linux 배포판 정보를 감지합니다.
|
|
29
|
+
* @returns {Promise<Object>} { id, name, version, packageManager }
|
|
30
|
+
*/
|
|
31
|
+
async function detectLinuxDistro() {
|
|
32
|
+
const result = {
|
|
33
|
+
id: 'unknown',
|
|
34
|
+
name: 'Linux',
|
|
35
|
+
version: '',
|
|
36
|
+
packageManager: null,
|
|
37
|
+
packageManagerName: ''
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// /etc/os-release 파일에서 배포판 정보 읽기
|
|
41
|
+
try {
|
|
42
|
+
if (existsSync('/etc/os-release')) {
|
|
43
|
+
const content = readFileSync('/etc/os-release', 'utf8');
|
|
44
|
+
const lines = content.split('\n');
|
|
45
|
+
|
|
46
|
+
for (const line of lines) {
|
|
47
|
+
const [key, ...valueParts] = line.split('=');
|
|
48
|
+
const value = valueParts.join('=').replace(/^["']|["']$/g, '');
|
|
49
|
+
|
|
50
|
+
if (key === 'ID') result.id = value.toLowerCase();
|
|
51
|
+
if (key === 'NAME') result.name = value;
|
|
52
|
+
if (key === 'VERSION_ID') result.version = value;
|
|
53
|
+
if (key === 'ID_LIKE') result.idLike = value.toLowerCase();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch (e) {
|
|
57
|
+
// 파일 읽기 실패 시 무시
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 패키지 매니저 감지
|
|
61
|
+
const packageManagers = [
|
|
62
|
+
{ cmd: 'apt', id: ['ubuntu', 'debian', 'linuxmint', 'pop', 'elementary', 'zorin', 'kali'], name: 'APT' },
|
|
63
|
+
{ cmd: 'dnf', id: ['fedora', 'rhel', 'centos', 'rocky', 'alma', 'nobara'], name: 'DNF' },
|
|
64
|
+
{ cmd: 'yum', id: ['centos', 'rhel', 'amazon'], name: 'YUM' },
|
|
65
|
+
{ cmd: 'pacman', id: ['arch', 'manjaro', 'endeavouros', 'garuda'], name: 'Pacman' },
|
|
66
|
+
{ cmd: 'zypper', id: ['opensuse', 'suse'], name: 'Zypper' },
|
|
67
|
+
{ cmd: 'apk', id: ['alpine'], name: 'APK' },
|
|
68
|
+
{ cmd: 'emerge', id: ['gentoo'], name: 'Portage' },
|
|
69
|
+
{ cmd: 'brew', id: [], name: 'Homebrew' } // fallback for any Linux with brew
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
// 배포판 ID로 먼저 매칭
|
|
73
|
+
for (const pm of packageManagers) {
|
|
74
|
+
if (pm.id.includes(result.id) || (result.idLike && pm.id.some(id => result.idLike.includes(id)))) {
|
|
75
|
+
const hasCmd = await getCommandPath(pm.cmd);
|
|
76
|
+
if (hasCmd) {
|
|
77
|
+
result.packageManager = pm.cmd;
|
|
78
|
+
result.packageManagerName = pm.name;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ID 매칭 실패 시 설치된 패키지 매니저로 감지
|
|
85
|
+
if (!result.packageManager) {
|
|
86
|
+
for (const pm of packageManagers) {
|
|
87
|
+
const hasCmd = await getCommandPath(pm.cmd);
|
|
88
|
+
if (hasCmd) {
|
|
89
|
+
result.packageManager = pm.cmd;
|
|
90
|
+
result.packageManagerName = pm.name;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* macOS 버전 정보를 가져옵니다.
|
|
101
|
+
* @returns {Promise<Object>} { name, version }
|
|
102
|
+
*/
|
|
103
|
+
async function getMacOSInfo() {
|
|
104
|
+
const result = { name: 'macOS', version: '' };
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const { stdout } = await execAsync('sw_vers -productVersion', { encoding: 'utf8' });
|
|
108
|
+
result.version = stdout.trim();
|
|
109
|
+
} catch (e) {
|
|
110
|
+
// 실패 시 무시
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
|
|
26
116
|
/**
|
|
27
117
|
* 특정 명령어의 실행 파일 경로를 찾습니다.
|
|
28
118
|
* @param {string} command - 찾을 명령어 이름
|
|
@@ -166,26 +256,161 @@ export async function getSystemInfoString(options = {}) {
|
|
|
166
256
|
return lines.join('\n');
|
|
167
257
|
}
|
|
168
258
|
|
|
259
|
+
/**
|
|
260
|
+
* 명령어별 설치 방법을 반환합니다.
|
|
261
|
+
* @param {string} command - 명령어 이름
|
|
262
|
+
* @param {string} os - OS 타입
|
|
263
|
+
* @param {Object} linuxDistro - Linux 배포판 정보
|
|
264
|
+
* @returns {Object} { primary, alternatives, url }
|
|
265
|
+
*/
|
|
266
|
+
function getInstallInstructions(command, os, linuxDistro = null) {
|
|
267
|
+
const instructions = {
|
|
268
|
+
ripgrep: {
|
|
269
|
+
macos: {
|
|
270
|
+
primary: 'brew install ripgrep',
|
|
271
|
+
alternatives: ['cargo install ripgrep'],
|
|
272
|
+
url: 'https://github.com/BurntSushi/ripgrep#installation'
|
|
273
|
+
},
|
|
274
|
+
linux: {
|
|
275
|
+
apt: { primary: 'sudo apt install ripgrep', alternatives: [] },
|
|
276
|
+
dnf: { primary: 'sudo dnf install ripgrep', alternatives: [] },
|
|
277
|
+
yum: { primary: 'sudo yum install ripgrep', alternatives: [] },
|
|
278
|
+
pacman: { primary: 'sudo pacman -S ripgrep', alternatives: [] },
|
|
279
|
+
zypper: { primary: 'sudo zypper install ripgrep', alternatives: [] },
|
|
280
|
+
apk: { primary: 'sudo apk add ripgrep', alternatives: [] },
|
|
281
|
+
brew: { primary: 'brew install ripgrep', alternatives: [] },
|
|
282
|
+
default: { primary: 'cargo install ripgrep', alternatives: ['brew install ripgrep'] },
|
|
283
|
+
url: 'https://github.com/BurntSushi/ripgrep#installation'
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
node: {
|
|
287
|
+
macos: {
|
|
288
|
+
primary: 'brew install node',
|
|
289
|
+
alternatives: ['Use nvm: curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash'],
|
|
290
|
+
url: 'https://nodejs.org/'
|
|
291
|
+
},
|
|
292
|
+
linux: {
|
|
293
|
+
apt: { primary: 'sudo apt install nodejs npm', alternatives: ['curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - && sudo apt install -y nodejs'] },
|
|
294
|
+
dnf: { primary: 'sudo dnf install nodejs npm', alternatives: [] },
|
|
295
|
+
yum: { primary: 'sudo yum install nodejs npm', alternatives: [] },
|
|
296
|
+
pacman: { primary: 'sudo pacman -S nodejs npm', alternatives: [] },
|
|
297
|
+
zypper: { primary: 'sudo zypper install nodejs npm', alternatives: [] },
|
|
298
|
+
apk: { primary: 'sudo apk add nodejs npm', alternatives: [] },
|
|
299
|
+
brew: { primary: 'brew install node', alternatives: [] },
|
|
300
|
+
default: { primary: 'Use nvm: curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash', alternatives: [] },
|
|
301
|
+
url: 'https://nodejs.org/'
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
bash: {
|
|
305
|
+
macos: {
|
|
306
|
+
primary: 'bash is built-in on macOS',
|
|
307
|
+
alternatives: [],
|
|
308
|
+
url: null
|
|
309
|
+
},
|
|
310
|
+
linux: {
|
|
311
|
+
apt: { primary: 'sudo apt install bash', alternatives: [] },
|
|
312
|
+
dnf: { primary: 'sudo dnf install bash', alternatives: [] },
|
|
313
|
+
yum: { primary: 'sudo yum install bash', alternatives: [] },
|
|
314
|
+
pacman: { primary: 'sudo pacman -S bash', alternatives: [] },
|
|
315
|
+
zypper: { primary: 'sudo zypper install bash', alternatives: [] },
|
|
316
|
+
apk: { primary: 'sudo apk add bash', alternatives: [] },
|
|
317
|
+
default: { primary: 'Install bash via your package manager', alternatives: [] },
|
|
318
|
+
url: null
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
python: {
|
|
322
|
+
macos: {
|
|
323
|
+
primary: 'brew install python3',
|
|
324
|
+
alternatives: [],
|
|
325
|
+
url: 'https://www.python.org/downloads/'
|
|
326
|
+
},
|
|
327
|
+
linux: {
|
|
328
|
+
apt: { primary: 'sudo apt install python3', alternatives: [] },
|
|
329
|
+
dnf: { primary: 'sudo dnf install python3', alternatives: [] },
|
|
330
|
+
yum: { primary: 'sudo yum install python3', alternatives: [] },
|
|
331
|
+
pacman: { primary: 'sudo pacman -S python', alternatives: [] },
|
|
332
|
+
zypper: { primary: 'sudo zypper install python3', alternatives: [] },
|
|
333
|
+
apk: { primary: 'sudo apk add python3', alternatives: [] },
|
|
334
|
+
brew: { primary: 'brew install python3', alternatives: [] },
|
|
335
|
+
default: { primary: 'Install python3 via your package manager', alternatives: [] },
|
|
336
|
+
url: 'https://www.python.org/downloads/'
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const cmdInstructions = instructions[command];
|
|
342
|
+
if (!cmdInstructions) {
|
|
343
|
+
return { primary: `Install ${command}`, alternatives: [], url: null };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (os === 'macos') {
|
|
347
|
+
return cmdInstructions.macos;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (os === 'linux' && linuxDistro) {
|
|
351
|
+
const pm = linuxDistro.packageManager;
|
|
352
|
+
const linuxInst = cmdInstructions.linux;
|
|
353
|
+
|
|
354
|
+
if (pm && linuxInst[pm]) {
|
|
355
|
+
return {
|
|
356
|
+
...linuxInst[pm],
|
|
357
|
+
url: linuxInst.url
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
...linuxInst.default,
|
|
363
|
+
url: linuxInst.url
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return { primary: `Install ${command}`, alternatives: [], url: null };
|
|
368
|
+
}
|
|
369
|
+
|
|
169
370
|
/**
|
|
170
371
|
* 필수 의존성을 체크하고 문제가 있으면 설치 방법을 안내합니다.
|
|
171
372
|
* @param {Object} options - 옵션
|
|
172
373
|
* @param {boolean} options.skipPython - Python 체크를 생략할지 여부
|
|
173
|
-
* @returns {Promise<Object>} { success: boolean, issues: Array, os: string }
|
|
374
|
+
* @returns {Promise<Object>} { success: boolean, issues: Array, os: string, osInfo: Object }
|
|
174
375
|
*/
|
|
175
376
|
export async function checkDependencies(options = {}) {
|
|
176
377
|
const { skipPython = false } = options;
|
|
177
378
|
const info = await getSystemInfo({ skipPython });
|
|
178
379
|
const issues = [];
|
|
179
380
|
|
|
381
|
+
// OS 상세 정보 수집
|
|
382
|
+
let osInfo = { name: info.os, version: '' };
|
|
383
|
+
let linuxDistro = null;
|
|
384
|
+
|
|
385
|
+
if (info.os === 'linux') {
|
|
386
|
+
linuxDistro = await detectLinuxDistro();
|
|
387
|
+
osInfo = {
|
|
388
|
+
name: linuxDistro.name,
|
|
389
|
+
version: linuxDistro.version,
|
|
390
|
+
id: linuxDistro.id,
|
|
391
|
+
packageManager: linuxDistro.packageManager,
|
|
392
|
+
packageManagerName: linuxDistro.packageManagerName
|
|
393
|
+
};
|
|
394
|
+
} else if (info.os === 'macos') {
|
|
395
|
+
const macInfo = await getMacOSInfo();
|
|
396
|
+
osInfo = macInfo;
|
|
397
|
+
}
|
|
398
|
+
|
|
180
399
|
// Windows 체크
|
|
181
400
|
if (info.os === 'windows') {
|
|
182
401
|
return {
|
|
183
402
|
success: false,
|
|
184
403
|
os: 'windows',
|
|
404
|
+
osInfo: { name: 'Windows', version: '' },
|
|
185
405
|
issues: [{
|
|
186
406
|
type: 'unsupported_os',
|
|
187
407
|
message: 'Windows is not supported',
|
|
188
|
-
details: 'This application
|
|
408
|
+
details: 'This application requires macOS or Linux.',
|
|
409
|
+
suggestions: [
|
|
410
|
+
'Use Windows Subsystem for Linux (WSL2)',
|
|
411
|
+
'Use a Linux virtual machine',
|
|
412
|
+
'Use Docker with a Linux container'
|
|
413
|
+
]
|
|
189
414
|
}],
|
|
190
415
|
warnings: []
|
|
191
416
|
};
|
|
@@ -193,33 +418,27 @@ export async function checkDependencies(options = {}) {
|
|
|
193
418
|
|
|
194
419
|
// ripgrep 체크 (필수)
|
|
195
420
|
if (!info.commands.hasRipgrep) {
|
|
196
|
-
const
|
|
197
|
-
? 'brew install ripgrep'
|
|
198
|
-
: info.os === 'linux'
|
|
199
|
-
? 'apt install ripgrep # or brew install ripgrep # or cargo install ripgrep'
|
|
200
|
-
: 'Visit https://github.com/BurntSushi/ripgrep#installation';
|
|
201
|
-
|
|
421
|
+
const inst = getInstallInstructions('ripgrep', info.os, linuxDistro);
|
|
202
422
|
issues.push({
|
|
203
423
|
type: 'missing_command',
|
|
204
|
-
command: 'ripgrep
|
|
205
|
-
|
|
206
|
-
|
|
424
|
+
command: 'ripgrep',
|
|
425
|
+
displayName: 'ripgrep (rg)',
|
|
426
|
+
description: 'Fast regex-based code search tool',
|
|
427
|
+
message: 'ripgrep is required for code search functionality',
|
|
428
|
+
install: inst
|
|
207
429
|
});
|
|
208
430
|
}
|
|
209
431
|
|
|
210
|
-
// node 체크 (필수)
|
|
432
|
+
// node 체크 (필수) - 이 메시지를 보려면 node가 있어야 하지만 완전성을 위해 유지
|
|
211
433
|
if (!info.commands.hasNode) {
|
|
212
|
-
const
|
|
213
|
-
? 'brew install node'
|
|
214
|
-
: info.os === 'linux'
|
|
215
|
-
? 'Visit https://nodejs.org/ or use nvm: https://github.com/nvm-sh/nvm'
|
|
216
|
-
: 'Visit https://nodejs.org/';
|
|
217
|
-
|
|
434
|
+
const inst = getInstallInstructions('node', info.os, linuxDistro);
|
|
218
435
|
issues.push({
|
|
219
436
|
type: 'missing_command',
|
|
220
437
|
command: 'node',
|
|
221
|
-
|
|
222
|
-
|
|
438
|
+
displayName: 'Node.js',
|
|
439
|
+
description: 'JavaScript runtime environment',
|
|
440
|
+
message: 'Node.js is required to run this application',
|
|
441
|
+
install: inst
|
|
223
442
|
});
|
|
224
443
|
}
|
|
225
444
|
|
|
@@ -227,42 +446,37 @@ export async function checkDependencies(options = {}) {
|
|
|
227
446
|
if (!info.commands.hasBash) {
|
|
228
447
|
const shellPath = await getCommandPath('sh');
|
|
229
448
|
if (!shellPath) {
|
|
230
|
-
const
|
|
231
|
-
? 'bash is built-in on macOS. Please check your system.'
|
|
232
|
-
: info.os === 'linux'
|
|
233
|
-
? 'apt install bash # or check your package manager'
|
|
234
|
-
: 'bash or sh shell is required';
|
|
235
|
-
|
|
449
|
+
const inst = getInstallInstructions('bash', info.os, linuxDistro);
|
|
236
450
|
issues.push({
|
|
237
451
|
type: 'missing_command',
|
|
238
|
-
command: 'bash
|
|
239
|
-
|
|
240
|
-
|
|
452
|
+
command: 'bash',
|
|
453
|
+
displayName: 'Bash Shell',
|
|
454
|
+
description: 'Unix shell for command execution',
|
|
455
|
+
message: 'A compatible shell (bash or sh) is required',
|
|
456
|
+
install: inst
|
|
241
457
|
});
|
|
242
458
|
}
|
|
243
459
|
}
|
|
244
460
|
|
|
245
|
-
// python 체크 (선택사항
|
|
461
|
+
// python 체크 (선택사항)
|
|
246
462
|
const warnings = [];
|
|
247
463
|
if (!skipPython && !info.commands.hasPython) {
|
|
248
|
-
const
|
|
249
|
-
? 'brew install python3'
|
|
250
|
-
: info.os === 'linux'
|
|
251
|
-
? 'apt install python3 # or yum install python3'
|
|
252
|
-
: 'Visit https://www.python.org/downloads/';
|
|
253
|
-
|
|
464
|
+
const inst = getInstallInstructions('python', info.os, linuxDistro);
|
|
254
465
|
warnings.push({
|
|
255
466
|
type: 'optional_command',
|
|
256
|
-
command: 'python
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
467
|
+
command: 'python',
|
|
468
|
+
displayName: 'Python 3',
|
|
469
|
+
description: 'Programming language for web scraping and scripting',
|
|
470
|
+
message: 'Python is optional but enables additional features',
|
|
471
|
+
install: inst,
|
|
472
|
+
disabledFeatures: ['fetch_web_page', 'run_python_code']
|
|
260
473
|
});
|
|
261
474
|
}
|
|
262
475
|
|
|
263
476
|
return {
|
|
264
477
|
success: issues.length === 0,
|
|
265
478
|
os: info.os,
|
|
479
|
+
osInfo,
|
|
266
480
|
issues,
|
|
267
481
|
warnings
|
|
268
482
|
};
|