makecc 0.2.7 → 0.2.10

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.
@@ -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
  import type { ApiSettings } from './workflowAIService';
5
7
 
6
8
  export interface GeneratedSkill {
@@ -21,6 +23,24 @@ export interface SkillGenerationResult {
21
23
  error?: string;
22
24
  }
23
25
 
26
+ export type SkillProgressStep =
27
+ | 'started'
28
+ | 'analyzing'
29
+ | 'designing'
30
+ | 'generating'
31
+ | 'saving'
32
+ | 'installing'
33
+ | 'completed'
34
+ | 'error';
35
+
36
+ export interface SkillProgressEvent {
37
+ step: SkillProgressStep;
38
+ message: string;
39
+ detail?: string;
40
+ }
41
+
42
+ export type SkillProgressCallback = (event: SkillProgressEvent) => void;
43
+
24
44
  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.
25
45
 
26
46
  Your response must follow this exact JSON schema:
@@ -106,14 +126,44 @@ export class SkillGeneratorService {
106
126
  return new Anthropic({ apiKey: envApiKey });
107
127
  }
108
128
 
109
- async generate(prompt: string, settings?: ApiSettings): Promise<SkillGenerationResult> {
129
+ async generate(
130
+ prompt: string,
131
+ settings?: ApiSettings,
132
+ onProgress?: SkillProgressCallback
133
+ ): Promise<SkillGenerationResult> {
134
+ const progress = onProgress || (() => {});
135
+
136
+ progress({
137
+ step: 'started',
138
+ message: '✨ 스킬 생성을 시작합니다',
139
+ detail: `요청: "${prompt.slice(0, 50)}${prompt.length > 50 ? '...' : ''}"`,
140
+ });
141
+
110
142
  const client = this.getClient(settings);
111
143
 
144
+ progress({
145
+ step: 'analyzing',
146
+ message: '🔍 요청을 분석하고 있어요',
147
+ detail: '어떤 스킬이 필요한지 파악 중...',
148
+ });
149
+
112
150
  const userPrompt = `Create a skill for: "${prompt}"
113
151
 
114
152
  Generate complete, working code. Respond with JSON only.`;
115
153
 
116
154
  try {
155
+ progress({
156
+ step: 'designing',
157
+ message: '📝 스킬 구조를 설계하고 있어요',
158
+ detail: 'AI가 최적의 스킬 구조를 결정 중...',
159
+ });
160
+
161
+ progress({
162
+ step: 'generating',
163
+ message: '⚙️ 코드를 생성하고 있어요',
164
+ detail: 'Python 스크립트와 설정 파일 작성 중...',
165
+ });
166
+
117
167
  const response = await client.messages.create({
118
168
  model: 'claude-sonnet-4-20250514',
119
169
  max_tokens: 8192,
@@ -132,6 +182,10 @@ Generate complete, working code. Respond with JSON only.`;
132
182
  }
133
183
 
134
184
  if (!responseText) {
185
+ progress({
186
+ step: 'error',
187
+ message: '❌ AI 응답을 받지 못했습니다',
188
+ });
135
189
  return { success: false, error: 'AI 응답을 받지 못했습니다.' };
136
190
  }
137
191
 
@@ -143,13 +197,52 @@ Generate complete, working code. Respond with JSON only.`;
143
197
  skill = JSON.parse(fullJson);
144
198
  } catch {
145
199
  console.error('Failed to parse skill response:', fullJson.slice(0, 500));
200
+ progress({
201
+ step: 'error',
202
+ message: '❌ AI 응답을 파싱할 수 없습니다',
203
+ detail: '다시 시도해주세요',
204
+ });
146
205
  return { success: false, error: 'AI 응답을 파싱할 수 없습니다. 다시 시도해주세요.' };
147
206
  }
148
207
 
208
+ progress({
209
+ step: 'saving',
210
+ message: '💾 파일을 저장하고 있어요',
211
+ detail: `${skill.files.length}개 파일 저장 중...`,
212
+ });
213
+
149
214
  // 파일 저장
150
215
  const skillPath = path.join(this.projectRoot, '.claude', 'skills', skill.skillId);
151
216
  await this.saveSkillFiles(skillPath, skill.files);
152
217
 
218
+ // requirements.txt가 있으면 의존성 설치
219
+ const requirementsPath = path.join(skillPath, 'requirements.txt');
220
+ if (existsSync(requirementsPath)) {
221
+ progress({
222
+ step: 'installing',
223
+ message: '📦 패키지를 설치하고 있어요',
224
+ detail: 'pip install 실행 중...',
225
+ });
226
+
227
+ try {
228
+ await this.installDependencies(requirementsPath, progress);
229
+ } catch (installError) {
230
+ // 설치 실패해도 스킬 생성은 성공으로 처리
231
+ console.error('Dependency installation failed:', installError);
232
+ progress({
233
+ step: 'installing',
234
+ message: '⚠️ 일부 패키지 설치에 실패했어요',
235
+ detail: '나중에 수동으로 설치해주세요',
236
+ });
237
+ }
238
+ }
239
+
240
+ progress({
241
+ step: 'completed',
242
+ message: '✅ 스킬이 생성되었습니다!',
243
+ detail: `${skill.skillName} → ${skillPath}`,
244
+ });
245
+
153
246
  return {
154
247
  success: true,
155
248
  skill,
@@ -158,6 +251,11 @@ Generate complete, working code. Respond with JSON only.`;
158
251
  } catch (error) {
159
252
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
160
253
  console.error('Skill generation error:', errorMessage);
254
+ progress({
255
+ step: 'error',
256
+ message: '❌ 스킬 생성에 실패했습니다',
257
+ detail: errorMessage,
258
+ });
161
259
  return { success: false, error: errorMessage };
162
260
  }
163
261
  }
@@ -181,6 +279,107 @@ Generate complete, working code. Respond with JSON only.`;
181
279
  console.log(`Saved: ${filePath}`);
182
280
  }
183
281
  }
282
+
283
+ private async installDependencies(
284
+ requirementsPath: string,
285
+ progress: SkillProgressCallback
286
+ ): Promise<void> {
287
+ return new Promise((resolve, reject) => {
288
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '';
289
+ const venvPythonPath = path.join(homeDir, '.claude', 'venv', 'bin', 'python');
290
+
291
+ let command: string;
292
+ let args: string[];
293
+
294
+ // uv를 우선 사용 (10-100x 빠름)
295
+ // uv pip install --python <venv-python> -r requirements.txt
296
+ const useUv = this.checkCommandExists('uv');
297
+
298
+ if (useUv && existsSync(venvPythonPath)) {
299
+ command = 'uv';
300
+ args = ['pip', 'install', '--python', venvPythonPath, '-r', requirementsPath];
301
+ progress({
302
+ step: 'installing',
303
+ message: '⚡ uv로 패키지 설치 중 (고속)',
304
+ detail: 'uv pip install 실행 중...',
305
+ });
306
+ } else if (existsSync(path.join(homeDir, '.claude', 'venv', 'bin', 'pip'))) {
307
+ // fallback: 전역 venv pip 사용
308
+ command = path.join(homeDir, '.claude', 'venv', 'bin', 'pip');
309
+ args = ['install', '-r', requirementsPath];
310
+ progress({
311
+ step: 'installing',
312
+ message: '📦 pip으로 패키지 설치 중',
313
+ detail: 'pip install 실행 중...',
314
+ });
315
+ } else {
316
+ // fallback: 시스템 pip 사용
317
+ command = 'pip3';
318
+ args = ['install', '-r', requirementsPath];
319
+ progress({
320
+ step: 'installing',
321
+ message: '📦 pip으로 패키지 설치 중',
322
+ detail: 'pip install 실행 중...',
323
+ });
324
+ }
325
+
326
+ console.log(`Installing dependencies: ${command} ${args.join(' ')}`);
327
+
328
+ const proc = spawn(command, args, {
329
+ stdio: ['ignore', 'pipe', 'pipe'],
330
+ });
331
+
332
+ let output = '';
333
+ let errorOutput = '';
334
+
335
+ proc.stdout?.on('data', (data) => {
336
+ output += data.toString();
337
+ const lines = data.toString().trim().split('\n');
338
+ for (const line of lines) {
339
+ if (line.includes('Successfully installed') || line.includes('Requirement already satisfied') || line.includes('Installed')) {
340
+ progress({
341
+ step: 'installing',
342
+ message: '📦 패키지 설치 중',
343
+ detail: line.slice(0, 60) + (line.length > 60 ? '...' : ''),
344
+ });
345
+ }
346
+ }
347
+ });
348
+
349
+ proc.stderr?.on('data', (data) => {
350
+ errorOutput += data.toString();
351
+ });
352
+
353
+ proc.on('close', (code) => {
354
+ if (code === 0) {
355
+ progress({
356
+ step: 'installing',
357
+ message: '✅ 패키지 설치 완료',
358
+ detail: '모든 의존성이 설치되었습니다',
359
+ });
360
+ resolve();
361
+ } else {
362
+ console.error('Package install failed:', errorOutput);
363
+ reject(new Error(`Package install failed with code ${code}`));
364
+ }
365
+ });
366
+
367
+ proc.on('error', (err) => {
368
+ console.error('Failed to start package installer:', err);
369
+ reject(err);
370
+ });
371
+ });
372
+ }
373
+
374
+ private checkCommandExists(cmd: string): boolean {
375
+ try {
376
+ const { execSync } = require('child_process');
377
+ execSync(`which ${cmd}`, { stdio: 'ignore' });
378
+ return true;
379
+ } catch {
380
+ return false;
381
+ }
382
+ }
184
383
  }
185
384
 
186
385
  export const skillGeneratorService = new SkillGeneratorService();