aiexecode 1.0.94 → 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.
Potentially problematic release.
This version of aiexecode might be problematic. Click here for more details.
- package/README.md +198 -88
- package/index.js +310 -86
- package/mcp-agent-lib/src/mcp_message_logger.js +17 -16
- package/package.json +4 -4
- package/payload_viewer/out/404/index.html +1 -1
- package/payload_viewer/out/404.html +1 -1
- package/payload_viewer/out/_next/static/chunks/{37d0cd2587a38f79.js → b6c0459f3789d25c.js} +1 -1
- package/payload_viewer/out/_next/static/chunks/b75131b58f8ca46a.css +3 -0
- package/payload_viewer/out/index.html +1 -1
- package/payload_viewer/out/index.txt +3 -3
- package/payload_viewer/web_server.js +361 -0
- package/prompts/completion_judge.txt +4 -0
- package/prompts/orchestrator.txt +116 -3
- package/src/LLMClient/client.js +401 -18
- package/src/LLMClient/converters/responses-to-claude.js +67 -18
- package/src/LLMClient/converters/responses-to-zai.js +667 -0
- package/src/LLMClient/errors.js +30 -4
- package/src/LLMClient/index.js +5 -0
- package/src/ai_based/completion_judge.js +263 -186
- package/src/ai_based/orchestrator.js +171 -35
- package/src/commands/agents.js +70 -0
- package/src/commands/apikey.js +1 -1
- package/src/commands/bg.js +129 -0
- package/src/commands/commands.js +51 -0
- package/src/commands/debug.js +52 -0
- package/src/commands/help.js +11 -1
- package/src/commands/model.js +42 -7
- package/src/commands/reasoning_effort.js +2 -2
- package/src/commands/skills.js +46 -0
- package/src/config/ai_models.js +106 -6
- package/src/config/constants.js +71 -0
- package/src/config/feature_flags.js +6 -7
- package/src/frontend/App.js +108 -1
- package/src/frontend/components/AutocompleteMenu.js +7 -1
- package/src/frontend/components/BackgroundProcessList.js +175 -0
- package/src/frontend/components/ConversationItem.js +26 -10
- package/src/frontend/components/CurrentModelView.js +2 -2
- package/src/frontend/components/HelpView.js +106 -2
- package/src/frontend/components/Input.js +33 -11
- package/src/frontend/components/ModelListView.js +1 -1
- package/src/frontend/components/SetupWizard.js +51 -8
- package/src/frontend/hooks/useFileCompletion.js +467 -0
- package/src/frontend/utils/toolUIFormatter.js +261 -0
- package/src/system/agents_loader.js +289 -0
- package/src/system/ai_request.js +156 -12
- package/src/system/background_process.js +317 -0
- package/src/system/code_executer.js +496 -56
- package/src/system/command_parser.js +33 -3
- package/src/system/conversation_state.js +265 -0
- package/src/system/conversation_trimmer.js +132 -0
- package/src/system/custom_command_loader.js +386 -0
- package/src/system/file_integrity.js +73 -10
- package/src/system/log.js +10 -2
- package/src/system/output_helper.js +52 -9
- package/src/system/session.js +213 -58
- package/src/system/session_memory.js +30 -2
- package/src/system/skill_loader.js +318 -0
- package/src/system/system_info.js +254 -40
- package/src/system/tool_approval.js +10 -0
- package/src/system/tool_registry.js +15 -1
- package/src/system/ui_events.js +11 -0
- package/src/tools/code_editor.js +16 -10
- package/src/tools/file_reader.js +66 -9
- package/src/tools/glob.js +0 -3
- package/src/tools/ripgrep.js +5 -7
- package/src/tools/skill_tool.js +122 -0
- package/src/tools/web_downloader.js +0 -3
- package/src/util/clone.js +174 -0
- package/src/util/config.js +55 -2
- package/src/util/config_migration.js +174 -0
- package/src/util/debug_log.js +8 -2
- package/src/util/exit_handler.js +8 -0
- package/src/util/file_reference_parser.js +132 -0
- package/src/util/path_validator.js +178 -0
- package/src/util/prompt_loader.js +91 -1
- package/src/util/safe_fs.js +66 -3
- package/payload_viewer/out/_next/static/chunks/ecd2072ebf41611f.css +0 -3
- /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → 42iEoi-1o5MxNIZ1SWSvV}/_buildManifest.js +0 -0
- /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → 42iEoi-1o5MxNIZ1SWSvV}/_clientMiddlewareManifest.json +0 -0
- /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → 42iEoi-1o5MxNIZ1SWSvV}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill Loader
|
|
3
|
+
*
|
|
4
|
+
* Claude Code 스타일의 스킬 시스템 구현
|
|
5
|
+
* 스킬은 SKILL.md 파일이 포함된 폴더로, AI에게 특정 작업 수행 방법을 가르칩니다.
|
|
6
|
+
*
|
|
7
|
+
* 스킬 위치 (우선순위 순):
|
|
8
|
+
* 1. Project: CWD/.aiexe/skills/<skill-name>/SKILL.md
|
|
9
|
+
* 2. Personal: ~/.aiexe/skills/<skill-name>/SKILL.md
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { join, basename, dirname } from 'path';
|
|
13
|
+
import { safeReadFile, safeReaddir, safeStat, safeAccess } from '../util/safe_fs.js';
|
|
14
|
+
import { PERSONAL_SKILLS_DIR, PROJECT_SKILLS_DIR } from '../util/config.js';
|
|
15
|
+
import { createDebugLogger } from '../util/debug_log.js';
|
|
16
|
+
|
|
17
|
+
const debugLog = createDebugLogger('skill_loader.log', 'skill_loader');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 스킬 정보 객체
|
|
21
|
+
* @typedef {Object} Skill
|
|
22
|
+
* @property {string} name - 스킬 이름 (폴더명 또는 frontmatter의 name)
|
|
23
|
+
* @property {string} description - 스킬 설명
|
|
24
|
+
* @property {string} path - SKILL.md 파일 경로
|
|
25
|
+
* @property {string} directory - 스킬 폴더 경로
|
|
26
|
+
* @property {string} source - 'project' | 'personal'
|
|
27
|
+
* @property {Object} frontmatter - YAML frontmatter 파싱 결과
|
|
28
|
+
* @property {string} content - 마크다운 내용 (frontmatter 제외)
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* YAML frontmatter를 파싱합니다.
|
|
33
|
+
* @param {string} content - SKILL.md 파일 내용
|
|
34
|
+
* @returns {{ frontmatter: Object, content: string }}
|
|
35
|
+
*/
|
|
36
|
+
function parseFrontmatter(content) {
|
|
37
|
+
const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
|
|
38
|
+
const match = content.match(frontmatterRegex);
|
|
39
|
+
|
|
40
|
+
if (!match) {
|
|
41
|
+
return {
|
|
42
|
+
frontmatter: {},
|
|
43
|
+
content: content.trim()
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const yamlContent = match[1];
|
|
48
|
+
const markdownContent = match[2];
|
|
49
|
+
|
|
50
|
+
// 간단한 YAML 파싱 (key: value 형식만 지원)
|
|
51
|
+
const frontmatter = {};
|
|
52
|
+
const lines = yamlContent.split('\n');
|
|
53
|
+
|
|
54
|
+
for (const line of lines) {
|
|
55
|
+
const colonIndex = line.indexOf(':');
|
|
56
|
+
if (colonIndex > 0) {
|
|
57
|
+
const key = line.substring(0, colonIndex).trim();
|
|
58
|
+
let value = line.substring(colonIndex + 1).trim();
|
|
59
|
+
|
|
60
|
+
// 따옴표 제거
|
|
61
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
62
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
63
|
+
value = value.slice(1, -1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// boolean 변환
|
|
67
|
+
if (value === 'true') value = true;
|
|
68
|
+
else if (value === 'false') value = false;
|
|
69
|
+
|
|
70
|
+
frontmatter[key] = value;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
frontmatter,
|
|
76
|
+
content: markdownContent.trim()
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 단일 스킬 폴더를 로드합니다.
|
|
82
|
+
* @param {string} skillDir - 스킬 폴더 경로
|
|
83
|
+
* @param {string} source - 'project' | 'personal'
|
|
84
|
+
* @returns {Promise<Skill|null>}
|
|
85
|
+
*/
|
|
86
|
+
async function loadSkillFromDirectory(skillDir, source) {
|
|
87
|
+
const skillMdPath = join(skillDir, 'SKILL.md');
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
await safeAccess(skillMdPath);
|
|
91
|
+
} catch {
|
|
92
|
+
debugLog(`[loadSkillFromDirectory] No SKILL.md found in ${skillDir}`);
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const content = await safeReadFile(skillMdPath, 'utf8');
|
|
98
|
+
const { frontmatter, content: markdownContent } = parseFrontmatter(content);
|
|
99
|
+
|
|
100
|
+
const folderName = basename(skillDir);
|
|
101
|
+
const name = frontmatter.name || folderName;
|
|
102
|
+
|
|
103
|
+
// description이 없으면 마크다운 첫 단락 사용
|
|
104
|
+
let description = frontmatter.description;
|
|
105
|
+
if (!description && markdownContent) {
|
|
106
|
+
const firstParagraph = markdownContent.split('\n\n')[0];
|
|
107
|
+
description = firstParagraph.replace(/^#.*\n?/, '').trim().substring(0, 200);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const skill = {
|
|
111
|
+
name,
|
|
112
|
+
description: description || '',
|
|
113
|
+
path: skillMdPath,
|
|
114
|
+
directory: skillDir,
|
|
115
|
+
source,
|
|
116
|
+
frontmatter,
|
|
117
|
+
content: markdownContent
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
debugLog(`[loadSkillFromDirectory] Loaded skill: ${name} from ${source}`);
|
|
121
|
+
return skill;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
debugLog(`[loadSkillFromDirectory] Error loading skill from ${skillDir}: ${error.message}`);
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 지정된 디렉토리에서 모든 스킬을 검색합니다.
|
|
130
|
+
* @param {string} skillsDir - 스킬 폴더 경로
|
|
131
|
+
* @param {string} source - 'project' | 'personal'
|
|
132
|
+
* @returns {Promise<Skill[]>}
|
|
133
|
+
*/
|
|
134
|
+
async function discoverSkillsInDirectory(skillsDir, source) {
|
|
135
|
+
const skills = [];
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
await safeAccess(skillsDir);
|
|
139
|
+
} catch {
|
|
140
|
+
debugLog(`[discoverSkillsInDirectory] Skills directory not found: ${skillsDir}`);
|
|
141
|
+
return skills;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const entries = await safeReaddir(skillsDir);
|
|
146
|
+
|
|
147
|
+
for (const entry of entries) {
|
|
148
|
+
const entryPath = join(skillsDir, entry);
|
|
149
|
+
const stat = await safeStat(entryPath);
|
|
150
|
+
|
|
151
|
+
if (stat.isDirectory()) {
|
|
152
|
+
const skill = await loadSkillFromDirectory(entryPath, source);
|
|
153
|
+
if (skill) {
|
|
154
|
+
skills.push(skill);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
} catch (error) {
|
|
159
|
+
debugLog(`[discoverSkillsInDirectory] Error scanning ${skillsDir}: ${error.message}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return skills;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 모든 스킬을 검색합니다.
|
|
167
|
+
* Project 스킬이 Personal 스킬보다 우선순위가 높습니다.
|
|
168
|
+
* @returns {Promise<Skill[]>}
|
|
169
|
+
*/
|
|
170
|
+
export async function discoverAllSkills() {
|
|
171
|
+
debugLog('[discoverAllSkills] Starting skill discovery...');
|
|
172
|
+
|
|
173
|
+
const allSkills = new Map(); // name -> skill (중복 방지, 우선순위 적용)
|
|
174
|
+
|
|
175
|
+
// 1. Personal skills (낮은 우선순위)
|
|
176
|
+
const personalSkills = await discoverSkillsInDirectory(PERSONAL_SKILLS_DIR, 'personal');
|
|
177
|
+
for (const skill of personalSkills) {
|
|
178
|
+
allSkills.set(skill.name, skill);
|
|
179
|
+
}
|
|
180
|
+
debugLog(`[discoverAllSkills] Found ${personalSkills.length} personal skills`);
|
|
181
|
+
|
|
182
|
+
// 2. Project skills (높은 우선순위 - 덮어씀)
|
|
183
|
+
const projectSkillsDir = join(process.cwd(), PROJECT_SKILLS_DIR);
|
|
184
|
+
const projectSkills = await discoverSkillsInDirectory(projectSkillsDir, 'project');
|
|
185
|
+
for (const skill of projectSkills) {
|
|
186
|
+
allSkills.set(skill.name, skill);
|
|
187
|
+
}
|
|
188
|
+
debugLog(`[discoverAllSkills] Found ${projectSkills.length} project skills`);
|
|
189
|
+
|
|
190
|
+
const skills = Array.from(allSkills.values());
|
|
191
|
+
debugLog(`[discoverAllSkills] Total unique skills: ${skills.length}`);
|
|
192
|
+
|
|
193
|
+
return skills;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 이름으로 스킬을 찾습니다.
|
|
198
|
+
* @param {string} skillName - 스킬 이름
|
|
199
|
+
* @returns {Promise<Skill|null>}
|
|
200
|
+
*/
|
|
201
|
+
export async function findSkillByName(skillName) {
|
|
202
|
+
debugLog(`[findSkillByName] Looking for skill: ${skillName}`);
|
|
203
|
+
|
|
204
|
+
// Project 스킬 먼저 확인 (높은 우선순위)
|
|
205
|
+
const projectSkillDir = join(process.cwd(), PROJECT_SKILLS_DIR, skillName);
|
|
206
|
+
const projectSkill = await loadSkillFromDirectory(projectSkillDir, 'project');
|
|
207
|
+
if (projectSkill) {
|
|
208
|
+
debugLog(`[findSkillByName] Found project skill: ${skillName}`);
|
|
209
|
+
return projectSkill;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Personal 스킬 확인
|
|
213
|
+
const personalSkillDir = join(PERSONAL_SKILLS_DIR, skillName);
|
|
214
|
+
const personalSkill = await loadSkillFromDirectory(personalSkillDir, 'personal');
|
|
215
|
+
if (personalSkill) {
|
|
216
|
+
debugLog(`[findSkillByName] Found personal skill: ${skillName}`);
|
|
217
|
+
return personalSkill;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
debugLog(`[findSkillByName] Skill not found: ${skillName}`);
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* 스킬 내용에서 $ARGUMENTS를 실제 인자로 치환합니다.
|
|
226
|
+
* @param {string} content - 스킬 마크다운 내용
|
|
227
|
+
* @param {string} args - 사용자가 전달한 인자
|
|
228
|
+
* @returns {string}
|
|
229
|
+
*/
|
|
230
|
+
export function substituteArguments(content, args) {
|
|
231
|
+
if (!args) return content;
|
|
232
|
+
|
|
233
|
+
// $ARGUMENTS 치환
|
|
234
|
+
let result = content.replace(/\$ARGUMENTS/g, args);
|
|
235
|
+
|
|
236
|
+
// $ARGUMENTS가 없었으면 끝에 추가
|
|
237
|
+
if (result === content && args.trim()) {
|
|
238
|
+
result = content + '\n\nARGUMENTS: ' + args;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* 스킬을 로드하고 프롬프트로 변환합니다.
|
|
246
|
+
* @param {string} skillName - 스킬 이름
|
|
247
|
+
* @param {string} [args] - 스킬에 전달할 인자
|
|
248
|
+
* @returns {Promise<{ prompt: string, skill: Skill } | null>}
|
|
249
|
+
*/
|
|
250
|
+
export async function loadSkillAsPrompt(skillName, args = '') {
|
|
251
|
+
const skill = await findSkillByName(skillName);
|
|
252
|
+
if (!skill) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
let prompt = skill.content;
|
|
257
|
+
|
|
258
|
+
// 인자 치환
|
|
259
|
+
prompt = substituteArguments(prompt, args);
|
|
260
|
+
|
|
261
|
+
// 참조 파일 로드 (선택적)
|
|
262
|
+
// TODO: reference.md, examples.md 등 자동 로드
|
|
263
|
+
|
|
264
|
+
debugLog(`[loadSkillAsPrompt] Loaded skill '${skillName}' with ${args ? 'args' : 'no args'}`);
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
prompt,
|
|
268
|
+
skill
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* 스킬 목록을 포맷팅하여 반환합니다.
|
|
274
|
+
* @returns {Promise<string>}
|
|
275
|
+
*/
|
|
276
|
+
export async function formatSkillList() {
|
|
277
|
+
const skills = await discoverAllSkills();
|
|
278
|
+
|
|
279
|
+
if (skills.length === 0) {
|
|
280
|
+
return [
|
|
281
|
+
'No skills found.',
|
|
282
|
+
'',
|
|
283
|
+
'Skill locations:',
|
|
284
|
+
` Personal: ~/.aiexe/skills/<skill-name>/SKILL.md`,
|
|
285
|
+
` Project: .aiexe/skills/<skill-name>/SKILL.md`,
|
|
286
|
+
'',
|
|
287
|
+
'Create a skill by making a folder with a SKILL.md file.'
|
|
288
|
+
].join('\n');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const lines = ['Available Skills:', ''];
|
|
292
|
+
|
|
293
|
+
// 소스별 그룹화
|
|
294
|
+
const projectSkills = skills.filter(s => s.source === 'project');
|
|
295
|
+
const personalSkills = skills.filter(s => s.source === 'personal');
|
|
296
|
+
|
|
297
|
+
if (projectSkills.length > 0) {
|
|
298
|
+
lines.push('Project Skills (.aiexe/skills/):');
|
|
299
|
+
for (const skill of projectSkills) {
|
|
300
|
+
const desc = skill.description ? ` - ${skill.description.substring(0, 60)}` : '';
|
|
301
|
+
lines.push(` /${skill.name}${desc}`);
|
|
302
|
+
}
|
|
303
|
+
lines.push('');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (personalSkills.length > 0) {
|
|
307
|
+
lines.push('Personal Skills (~/.aiexe/skills/):');
|
|
308
|
+
for (const skill of personalSkills) {
|
|
309
|
+
const desc = skill.description ? ` - ${skill.description.substring(0, 60)}` : '';
|
|
310
|
+
lines.push(` /${skill.name}${desc}`);
|
|
311
|
+
}
|
|
312
|
+
lines.push('');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
lines.push('Usage: /<skill-name> [arguments]');
|
|
316
|
+
|
|
317
|
+
return lines.join('\n');
|
|
318
|
+
}
|
|
@@ -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
|
};
|