makecc 0.2.5 → 0.2.8
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/dist/client/assets/index-C2Yw-VLu.js +150 -0
- package/dist/client/assets/index-v9IFpWkA.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/server/index.js +226 -1
- package/dist/server/services/configLoaderService.js +185 -0
- package/dist/server/services/nodeSyncService.js +313 -0
- package/dist/server/services/skillGeneratorService.js +159 -1
- package/package.json +1 -1
- package/server/index.ts +257 -2
- package/server/services/configLoaderService.ts +260 -0
- package/server/services/nodeSyncService.ts +384 -0
- package/server/services/skillGeneratorService.ts +200 -1
- package/dist/client/assets/index-DLT8bQFx.css +0 -1
- package/dist/client/assets/index-c7WYmobg.js +0 -147
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
export class NodeSyncService {
|
|
4
|
+
projectRoot;
|
|
5
|
+
constructor(projectRoot) {
|
|
6
|
+
this.projectRoot = projectRoot || process.env.MAKECC_PROJECT_PATH || process.cwd();
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* 노드 생성/수정 시 파일 동기화
|
|
10
|
+
*/
|
|
11
|
+
async syncNode(node) {
|
|
12
|
+
try {
|
|
13
|
+
switch (node.type) {
|
|
14
|
+
case 'skill':
|
|
15
|
+
return await this.syncSkillNode(node);
|
|
16
|
+
case 'subagent':
|
|
17
|
+
return await this.syncSubagentNode(node);
|
|
18
|
+
case 'command':
|
|
19
|
+
return await this.syncCommandNode(node);
|
|
20
|
+
case 'hook':
|
|
21
|
+
return await this.syncHookNode(node);
|
|
22
|
+
case 'input':
|
|
23
|
+
case 'output':
|
|
24
|
+
// 입력/출력 노드는 파일 저장 불필요
|
|
25
|
+
return { success: true };
|
|
26
|
+
default:
|
|
27
|
+
return { success: false, error: `Unknown node type: ${node.type}` };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
32
|
+
return { success: false, error: errorMessage };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* 노드 삭제 시 파일 삭제
|
|
37
|
+
*/
|
|
38
|
+
async deleteNode(node) {
|
|
39
|
+
try {
|
|
40
|
+
switch (node.type) {
|
|
41
|
+
case 'skill':
|
|
42
|
+
if (node.skillId) {
|
|
43
|
+
const skillPath = path.join(this.projectRoot, '.claude', 'skills', node.skillId);
|
|
44
|
+
await fs.rm(skillPath, { recursive: true, force: true });
|
|
45
|
+
}
|
|
46
|
+
break;
|
|
47
|
+
case 'subagent':
|
|
48
|
+
const agentName = this.toKebabCase(node.label);
|
|
49
|
+
const agentPath = path.join(this.projectRoot, '.claude', 'agents', `${agentName}.md`);
|
|
50
|
+
await fs.unlink(agentPath).catch(() => { });
|
|
51
|
+
break;
|
|
52
|
+
case 'command':
|
|
53
|
+
const cmdName = node.commandName || this.toKebabCase(node.label);
|
|
54
|
+
const cmdPath = path.join(this.projectRoot, '.claude', 'commands', `${cmdName}.md`);
|
|
55
|
+
await fs.unlink(cmdPath).catch(() => { });
|
|
56
|
+
break;
|
|
57
|
+
case 'hook':
|
|
58
|
+
await this.removeHookFromSettings(node);
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
return { success: true };
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
65
|
+
return { success: false, error: errorMessage };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 엣지 연결 시 관계 업데이트
|
|
70
|
+
*/
|
|
71
|
+
async syncEdge(edge, nodes) {
|
|
72
|
+
try {
|
|
73
|
+
const sourceNode = nodes.find(n => n.id === edge.source);
|
|
74
|
+
const targetNode = nodes.find(n => n.id === edge.target);
|
|
75
|
+
if (!sourceNode || !targetNode) {
|
|
76
|
+
return { success: false, error: 'Source or target node not found' };
|
|
77
|
+
}
|
|
78
|
+
// 서브에이전트 → 스킬 연결: 서브에이전트의 skills 필드 업데이트
|
|
79
|
+
if (sourceNode.type === 'subagent' && targetNode.type === 'skill') {
|
|
80
|
+
const skills = sourceNode.skills || [];
|
|
81
|
+
const skillId = targetNode.skillId || this.toKebabCase(targetNode.label);
|
|
82
|
+
if (!skills.includes(skillId)) {
|
|
83
|
+
skills.push(skillId);
|
|
84
|
+
sourceNode.skills = skills;
|
|
85
|
+
await this.syncSubagentNode(sourceNode);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// 스킬 → 서브에이전트 연결: 서브에이전트의 skills 필드 업데이트
|
|
89
|
+
if (sourceNode.type === 'skill' && targetNode.type === 'subagent') {
|
|
90
|
+
const skills = targetNode.skills || [];
|
|
91
|
+
const skillId = sourceNode.skillId || this.toKebabCase(sourceNode.label);
|
|
92
|
+
if (!skills.includes(skillId)) {
|
|
93
|
+
skills.push(skillId);
|
|
94
|
+
targetNode.skills = skills;
|
|
95
|
+
await this.syncSubagentNode(targetNode);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { success: true };
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
102
|
+
return { success: false, error: errorMessage };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* 엣지 삭제 시 관계 업데이트
|
|
107
|
+
*/
|
|
108
|
+
async removeEdge(edge, nodes) {
|
|
109
|
+
try {
|
|
110
|
+
const sourceNode = nodes.find(n => n.id === edge.source);
|
|
111
|
+
const targetNode = nodes.find(n => n.id === edge.target);
|
|
112
|
+
if (!sourceNode || !targetNode) {
|
|
113
|
+
return { success: true }; // 노드가 없으면 무시
|
|
114
|
+
}
|
|
115
|
+
// 서브에이전트에서 스킬 제거
|
|
116
|
+
if (sourceNode.type === 'subagent' && targetNode.type === 'skill') {
|
|
117
|
+
const skillId = targetNode.skillId || this.toKebabCase(targetNode.label);
|
|
118
|
+
sourceNode.skills = (sourceNode.skills || []).filter(s => s !== skillId);
|
|
119
|
+
await this.syncSubagentNode(sourceNode);
|
|
120
|
+
}
|
|
121
|
+
if (sourceNode.type === 'skill' && targetNode.type === 'subagent') {
|
|
122
|
+
const skillId = sourceNode.skillId || this.toKebabCase(sourceNode.label);
|
|
123
|
+
targetNode.skills = (targetNode.skills || []).filter(s => s !== skillId);
|
|
124
|
+
await this.syncSubagentNode(targetNode);
|
|
125
|
+
}
|
|
126
|
+
return { success: true };
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
130
|
+
return { success: false, error: errorMessage };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// ===== Private Methods =====
|
|
134
|
+
async syncSkillNode(node) {
|
|
135
|
+
const skillId = node.skillId || this.toKebabCase(node.label);
|
|
136
|
+
const skillPath = path.join(this.projectRoot, '.claude', 'skills', skillId);
|
|
137
|
+
const skillMdPath = path.join(skillPath, 'SKILL.md');
|
|
138
|
+
await fs.mkdir(skillPath, { recursive: true });
|
|
139
|
+
// 기존 파일이 있으면 읽어서 업데이트, 없으면 새로 생성
|
|
140
|
+
let content = '';
|
|
141
|
+
try {
|
|
142
|
+
content = await fs.readFile(skillMdPath, 'utf-8');
|
|
143
|
+
// frontmatter 업데이트
|
|
144
|
+
content = this.updateFrontmatter(content, {
|
|
145
|
+
name: skillId,
|
|
146
|
+
description: node.description || node.label,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// 새로 생성
|
|
151
|
+
content = `---
|
|
152
|
+
name: ${skillId}
|
|
153
|
+
description: ${node.description || node.label}
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
# ${node.label}
|
|
157
|
+
|
|
158
|
+
## 사용 시점
|
|
159
|
+
이 스킬은 다음 상황에서 사용됩니다:
|
|
160
|
+
- ${node.description || '설명을 추가하세요'}
|
|
161
|
+
|
|
162
|
+
## 사용 방법
|
|
163
|
+
|
|
164
|
+
\`\`\`bash
|
|
165
|
+
# 스킬 사용 방법을 여기에 작성하세요
|
|
166
|
+
\`\`\`
|
|
167
|
+
`;
|
|
168
|
+
}
|
|
169
|
+
await fs.writeFile(skillMdPath, content, 'utf-8');
|
|
170
|
+
return { success: true, path: skillPath };
|
|
171
|
+
}
|
|
172
|
+
async syncSubagentNode(node) {
|
|
173
|
+
const agentName = this.toKebabCase(node.label);
|
|
174
|
+
const agentsDir = path.join(this.projectRoot, '.claude', 'agents');
|
|
175
|
+
const agentPath = path.join(agentsDir, `${agentName}.md`);
|
|
176
|
+
await fs.mkdir(agentsDir, { recursive: true });
|
|
177
|
+
// Frontmatter 구성
|
|
178
|
+
const frontmatter = {
|
|
179
|
+
name: agentName,
|
|
180
|
+
description: node.description || node.label,
|
|
181
|
+
};
|
|
182
|
+
if (node.tools && node.tools.length > 0) {
|
|
183
|
+
frontmatter.tools = node.tools.join(', ');
|
|
184
|
+
}
|
|
185
|
+
if (node.model) {
|
|
186
|
+
frontmatter.model = node.model;
|
|
187
|
+
}
|
|
188
|
+
if (node.skills && node.skills.length > 0) {
|
|
189
|
+
frontmatter.skills = node.skills.join(', ');
|
|
190
|
+
}
|
|
191
|
+
const frontmatterStr = Object.entries(frontmatter)
|
|
192
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
193
|
+
.join('\n');
|
|
194
|
+
const content = `---
|
|
195
|
+
${frontmatterStr}
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
${node.systemPrompt || `You are ${node.label}.
|
|
199
|
+
|
|
200
|
+
${node.description || ''}
|
|
201
|
+
`}
|
|
202
|
+
`;
|
|
203
|
+
await fs.writeFile(agentPath, content, 'utf-8');
|
|
204
|
+
return { success: true, path: agentPath };
|
|
205
|
+
}
|
|
206
|
+
async syncCommandNode(node) {
|
|
207
|
+
const cmdName = node.commandName || this.toKebabCase(node.label);
|
|
208
|
+
const commandsDir = path.join(this.projectRoot, '.claude', 'commands');
|
|
209
|
+
const cmdPath = path.join(commandsDir, `${cmdName}.md`);
|
|
210
|
+
await fs.mkdir(commandsDir, { recursive: true });
|
|
211
|
+
const content = node.commandContent || `---
|
|
212
|
+
description: ${node.description || node.label}
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
${node.description || '커맨드 내용을 여기에 작성하세요'}
|
|
216
|
+
|
|
217
|
+
$ARGUMENTS
|
|
218
|
+
`;
|
|
219
|
+
await fs.writeFile(cmdPath, content, 'utf-8');
|
|
220
|
+
return { success: true, path: cmdPath };
|
|
221
|
+
}
|
|
222
|
+
async syncHookNode(node) {
|
|
223
|
+
const settingsPath = path.join(this.projectRoot, '.claude', 'settings.json');
|
|
224
|
+
// 기존 settings 읽기
|
|
225
|
+
let settings = {};
|
|
226
|
+
try {
|
|
227
|
+
const content = await fs.readFile(settingsPath, 'utf-8');
|
|
228
|
+
settings = JSON.parse(content);
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
// 파일 없음
|
|
232
|
+
}
|
|
233
|
+
// hooks 섹션 확인/생성
|
|
234
|
+
if (!settings.hooks) {
|
|
235
|
+
settings.hooks = {};
|
|
236
|
+
}
|
|
237
|
+
const hooks = settings.hooks;
|
|
238
|
+
const event = node.hookEvent || 'PreToolUse';
|
|
239
|
+
if (!hooks[event]) {
|
|
240
|
+
hooks[event] = [];
|
|
241
|
+
}
|
|
242
|
+
// 기존 훅 찾기 (같은 matcher로)
|
|
243
|
+
const eventHooks = hooks[event];
|
|
244
|
+
const existingIndex = eventHooks.findIndex(h => h.matcher === (node.hookMatcher || '*'));
|
|
245
|
+
const hookConfig = {
|
|
246
|
+
matcher: node.hookMatcher || '*',
|
|
247
|
+
hooks: [
|
|
248
|
+
{
|
|
249
|
+
type: 'command',
|
|
250
|
+
command: node.hookCommand || 'echo "Hook triggered"',
|
|
251
|
+
},
|
|
252
|
+
],
|
|
253
|
+
};
|
|
254
|
+
if (existingIndex >= 0) {
|
|
255
|
+
eventHooks[existingIndex] = hookConfig;
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
eventHooks.push(hookConfig);
|
|
259
|
+
}
|
|
260
|
+
// .claude 디렉토리 생성
|
|
261
|
+
await fs.mkdir(path.dirname(settingsPath), { recursive: true });
|
|
262
|
+
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
|
|
263
|
+
return { success: true, path: settingsPath };
|
|
264
|
+
}
|
|
265
|
+
async removeHookFromSettings(node) {
|
|
266
|
+
const settingsPath = path.join(this.projectRoot, '.claude', 'settings.json');
|
|
267
|
+
let settings = {};
|
|
268
|
+
try {
|
|
269
|
+
const content = await fs.readFile(settingsPath, 'utf-8');
|
|
270
|
+
settings = JSON.parse(content);
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const hooks = settings.hooks;
|
|
276
|
+
if (!hooks)
|
|
277
|
+
return;
|
|
278
|
+
const event = node.hookEvent || 'PreToolUse';
|
|
279
|
+
if (!hooks[event])
|
|
280
|
+
return;
|
|
281
|
+
const eventHooks = hooks[event];
|
|
282
|
+
hooks[event] = eventHooks.filter(h => h.matcher !== (node.hookMatcher || '*'));
|
|
283
|
+
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
|
|
284
|
+
}
|
|
285
|
+
updateFrontmatter(content, updates) {
|
|
286
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
287
|
+
if (!frontmatterMatch) {
|
|
288
|
+
// frontmatter 없으면 추가
|
|
289
|
+
const fm = Object.entries(updates)
|
|
290
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
291
|
+
.join('\n');
|
|
292
|
+
return `---\n${fm}\n---\n\n${content}`;
|
|
293
|
+
}
|
|
294
|
+
let frontmatter = frontmatterMatch[1];
|
|
295
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
296
|
+
const regex = new RegExp(`^${key}:.*$`, 'm');
|
|
297
|
+
if (regex.test(frontmatter)) {
|
|
298
|
+
frontmatter = frontmatter.replace(regex, `${key}: ${value}`);
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
frontmatter += `\n${key}: ${value}`;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return content.replace(/^---\n[\s\S]*?\n---/, `---\n${frontmatter}\n---`);
|
|
305
|
+
}
|
|
306
|
+
toKebabCase(str) {
|
|
307
|
+
return str
|
|
308
|
+
.toLowerCase()
|
|
309
|
+
.replace(/[^a-z0-9가-힣]+/g, '-')
|
|
310
|
+
.replace(/^-|-$/g, '');
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
export const nodeSyncService = new NodeSyncService();
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import Anthropic from '@anthropic-ai/sdk';
|
|
2
2
|
import * as fs from 'fs/promises';
|
|
3
3
|
import * as path from 'path';
|
|
4
|
+
import { existsSync } from 'fs';
|
|
5
|
+
import { spawn } from 'child_process';
|
|
4
6
|
const SYSTEM_PROMPT = `You are a Claude Code skill generator. You MUST respond with ONLY a valid JSON object. No markdown, no code blocks, no explanations - just pure JSON.
|
|
5
7
|
|
|
6
8
|
Your response must follow this exact JSON schema:
|
|
@@ -79,12 +81,33 @@ export class SkillGeneratorService {
|
|
|
79
81
|
}
|
|
80
82
|
return new Anthropic({ apiKey: envApiKey });
|
|
81
83
|
}
|
|
82
|
-
async generate(prompt, settings) {
|
|
84
|
+
async generate(prompt, settings, onProgress) {
|
|
85
|
+
const progress = onProgress || (() => { });
|
|
86
|
+
progress({
|
|
87
|
+
step: 'started',
|
|
88
|
+
message: '✨ 스킬 생성을 시작합니다',
|
|
89
|
+
detail: `요청: "${prompt.slice(0, 50)}${prompt.length > 50 ? '...' : ''}"`,
|
|
90
|
+
});
|
|
83
91
|
const client = this.getClient(settings);
|
|
92
|
+
progress({
|
|
93
|
+
step: 'analyzing',
|
|
94
|
+
message: '🔍 요청을 분석하고 있어요',
|
|
95
|
+
detail: '어떤 스킬이 필요한지 파악 중...',
|
|
96
|
+
});
|
|
84
97
|
const userPrompt = `Create a skill for: "${prompt}"
|
|
85
98
|
|
|
86
99
|
Generate complete, working code. Respond with JSON only.`;
|
|
87
100
|
try {
|
|
101
|
+
progress({
|
|
102
|
+
step: 'designing',
|
|
103
|
+
message: '📝 스킬 구조를 설계하고 있어요',
|
|
104
|
+
detail: 'AI가 최적의 스킬 구조를 결정 중...',
|
|
105
|
+
});
|
|
106
|
+
progress({
|
|
107
|
+
step: 'generating',
|
|
108
|
+
message: '⚙️ 코드를 생성하고 있어요',
|
|
109
|
+
detail: 'Python 스크립트와 설정 파일 작성 중...',
|
|
110
|
+
});
|
|
88
111
|
const response = await client.messages.create({
|
|
89
112
|
model: 'claude-sonnet-4-20250514',
|
|
90
113
|
max_tokens: 8192,
|
|
@@ -101,6 +124,10 @@ Generate complete, working code. Respond with JSON only.`;
|
|
|
101
124
|
}
|
|
102
125
|
}
|
|
103
126
|
if (!responseText) {
|
|
127
|
+
progress({
|
|
128
|
+
step: 'error',
|
|
129
|
+
message: '❌ AI 응답을 받지 못했습니다',
|
|
130
|
+
});
|
|
104
131
|
return { success: false, error: 'AI 응답을 받지 못했습니다.' };
|
|
105
132
|
}
|
|
106
133
|
// Prefill로 '{'를 보냈으니 응답 앞에 '{'를 붙임
|
|
@@ -111,11 +138,47 @@ Generate complete, working code. Respond with JSON only.`;
|
|
|
111
138
|
}
|
|
112
139
|
catch {
|
|
113
140
|
console.error('Failed to parse skill response:', fullJson.slice(0, 500));
|
|
141
|
+
progress({
|
|
142
|
+
step: 'error',
|
|
143
|
+
message: '❌ AI 응답을 파싱할 수 없습니다',
|
|
144
|
+
detail: '다시 시도해주세요',
|
|
145
|
+
});
|
|
114
146
|
return { success: false, error: 'AI 응답을 파싱할 수 없습니다. 다시 시도해주세요.' };
|
|
115
147
|
}
|
|
148
|
+
progress({
|
|
149
|
+
step: 'saving',
|
|
150
|
+
message: '💾 파일을 저장하고 있어요',
|
|
151
|
+
detail: `${skill.files.length}개 파일 저장 중...`,
|
|
152
|
+
});
|
|
116
153
|
// 파일 저장
|
|
117
154
|
const skillPath = path.join(this.projectRoot, '.claude', 'skills', skill.skillId);
|
|
118
155
|
await this.saveSkillFiles(skillPath, skill.files);
|
|
156
|
+
// requirements.txt가 있으면 의존성 설치
|
|
157
|
+
const requirementsPath = path.join(skillPath, 'requirements.txt');
|
|
158
|
+
if (existsSync(requirementsPath)) {
|
|
159
|
+
progress({
|
|
160
|
+
step: 'installing',
|
|
161
|
+
message: '📦 패키지를 설치하고 있어요',
|
|
162
|
+
detail: 'pip install 실행 중...',
|
|
163
|
+
});
|
|
164
|
+
try {
|
|
165
|
+
await this.installDependencies(requirementsPath, progress);
|
|
166
|
+
}
|
|
167
|
+
catch (installError) {
|
|
168
|
+
// 설치 실패해도 스킬 생성은 성공으로 처리
|
|
169
|
+
console.error('Dependency installation failed:', installError);
|
|
170
|
+
progress({
|
|
171
|
+
step: 'installing',
|
|
172
|
+
message: '⚠️ 일부 패키지 설치에 실패했어요',
|
|
173
|
+
detail: '나중에 수동으로 설치해주세요',
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
progress({
|
|
178
|
+
step: 'completed',
|
|
179
|
+
message: '✅ 스킬이 생성되었습니다!',
|
|
180
|
+
detail: `${skill.skillName} → ${skillPath}`,
|
|
181
|
+
});
|
|
119
182
|
return {
|
|
120
183
|
success: true,
|
|
121
184
|
skill,
|
|
@@ -125,6 +188,11 @@ Generate complete, working code. Respond with JSON only.`;
|
|
|
125
188
|
catch (error) {
|
|
126
189
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
127
190
|
console.error('Skill generation error:', errorMessage);
|
|
191
|
+
progress({
|
|
192
|
+
step: 'error',
|
|
193
|
+
message: '❌ 스킬 생성에 실패했습니다',
|
|
194
|
+
detail: errorMessage,
|
|
195
|
+
});
|
|
128
196
|
return { success: false, error: errorMessage };
|
|
129
197
|
}
|
|
130
198
|
}
|
|
@@ -141,5 +209,95 @@ Generate complete, working code. Respond with JSON only.`;
|
|
|
141
209
|
console.log(`Saved: ${filePath}`);
|
|
142
210
|
}
|
|
143
211
|
}
|
|
212
|
+
async installDependencies(requirementsPath, progress) {
|
|
213
|
+
return new Promise((resolve, reject) => {
|
|
214
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
215
|
+
const venvPythonPath = path.join(homeDir, '.claude', 'venv', 'bin', 'python');
|
|
216
|
+
let command;
|
|
217
|
+
let args;
|
|
218
|
+
// uv를 우선 사용 (10-100x 빠름)
|
|
219
|
+
// uv pip install --python <venv-python> -r requirements.txt
|
|
220
|
+
const useUv = this.checkCommandExists('uv');
|
|
221
|
+
if (useUv && existsSync(venvPythonPath)) {
|
|
222
|
+
command = 'uv';
|
|
223
|
+
args = ['pip', 'install', '--python', venvPythonPath, '-r', requirementsPath];
|
|
224
|
+
progress({
|
|
225
|
+
step: 'installing',
|
|
226
|
+
message: '⚡ uv로 패키지 설치 중 (고속)',
|
|
227
|
+
detail: 'uv pip install 실행 중...',
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
else if (existsSync(path.join(homeDir, '.claude', 'venv', 'bin', 'pip'))) {
|
|
231
|
+
// fallback: 전역 venv pip 사용
|
|
232
|
+
command = path.join(homeDir, '.claude', 'venv', 'bin', 'pip');
|
|
233
|
+
args = ['install', '-r', requirementsPath];
|
|
234
|
+
progress({
|
|
235
|
+
step: 'installing',
|
|
236
|
+
message: '📦 pip으로 패키지 설치 중',
|
|
237
|
+
detail: 'pip install 실행 중...',
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
// fallback: 시스템 pip 사용
|
|
242
|
+
command = 'pip3';
|
|
243
|
+
args = ['install', '-r', requirementsPath];
|
|
244
|
+
progress({
|
|
245
|
+
step: 'installing',
|
|
246
|
+
message: '📦 pip으로 패키지 설치 중',
|
|
247
|
+
detail: 'pip install 실행 중...',
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
console.log(`Installing dependencies: ${command} ${args.join(' ')}`);
|
|
251
|
+
const proc = spawn(command, args, {
|
|
252
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
253
|
+
});
|
|
254
|
+
let output = '';
|
|
255
|
+
let errorOutput = '';
|
|
256
|
+
proc.stdout?.on('data', (data) => {
|
|
257
|
+
output += data.toString();
|
|
258
|
+
const lines = data.toString().trim().split('\n');
|
|
259
|
+
for (const line of lines) {
|
|
260
|
+
if (line.includes('Successfully installed') || line.includes('Requirement already satisfied') || line.includes('Installed')) {
|
|
261
|
+
progress({
|
|
262
|
+
step: 'installing',
|
|
263
|
+
message: '📦 패키지 설치 중',
|
|
264
|
+
detail: line.slice(0, 60) + (line.length > 60 ? '...' : ''),
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
proc.stderr?.on('data', (data) => {
|
|
270
|
+
errorOutput += data.toString();
|
|
271
|
+
});
|
|
272
|
+
proc.on('close', (code) => {
|
|
273
|
+
if (code === 0) {
|
|
274
|
+
progress({
|
|
275
|
+
step: 'installing',
|
|
276
|
+
message: '✅ 패키지 설치 완료',
|
|
277
|
+
detail: '모든 의존성이 설치되었습니다',
|
|
278
|
+
});
|
|
279
|
+
resolve();
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
console.error('Package install failed:', errorOutput);
|
|
283
|
+
reject(new Error(`Package install failed with code ${code}`));
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
proc.on('error', (err) => {
|
|
287
|
+
console.error('Failed to start package installer:', err);
|
|
288
|
+
reject(err);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
checkCommandExists(cmd) {
|
|
293
|
+
try {
|
|
294
|
+
const { execSync } = require('child_process');
|
|
295
|
+
execSync(`which ${cmd}`, { stdio: 'ignore' });
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
144
302
|
}
|
|
145
303
|
export const skillGeneratorService = new SkillGeneratorService();
|