|
| 1 | +/** |
| 2 | + * CodeSyncer Skills Command |
| 3 | + * |
| 4 | + * Integration with skills.sh for skill discovery and installation. |
| 5 | + * |
| 6 | + * @codesyncer-context skills.sh API: https://skills.sh/api/skills |
| 7 | + * @codesyncer-decision [2026-01-23] skills.sh 연동으로 생태계 확장 |
| 8 | + */ |
| 9 | + |
| 10 | +import chalk from 'chalk'; |
| 11 | +import ora from 'ora'; |
| 12 | +import { spawn } from 'child_process'; |
| 13 | + |
| 14 | +const SKILLS_API_URL = 'https://skills.sh/api/skills'; |
| 15 | + |
| 16 | +interface Skill { |
| 17 | + id: string; |
| 18 | + name: string; |
| 19 | + installs: number; |
| 20 | + topSource: string; |
| 21 | +} |
| 22 | + |
| 23 | +interface SkillsApiResponse { |
| 24 | + skills: Skill[]; |
| 25 | +} |
| 26 | + |
| 27 | +/** |
| 28 | + * Fetch skills from skills.sh API |
| 29 | + */ |
| 30 | +async function fetchSkills(): Promise<Skill[]> { |
| 31 | + try { |
| 32 | + const response = await fetch(SKILLS_API_URL); |
| 33 | + if (!response.ok) { |
| 34 | + throw new Error(`HTTP ${response.status}`); |
| 35 | + } |
| 36 | + const data = await response.json() as SkillsApiResponse; |
| 37 | + return data.skills || []; |
| 38 | + } catch (error) { |
| 39 | + throw new Error(`Failed to fetch skills: ${error instanceof Error ? error.message : 'Unknown error'}`); |
| 40 | + } |
| 41 | +} |
| 42 | + |
| 43 | +/** |
| 44 | + * Display skills leaderboard |
| 45 | + */ |
| 46 | +function displayLeaderboard(skills: Skill[], isKo: boolean): void { |
| 47 | + console.log(); |
| 48 | + console.log(chalk.bold.cyan(isKo ? '🏆 Skills.sh 리더보드' : '🏆 Skills.sh Leaderboard')); |
| 49 | + console.log(chalk.gray('─'.repeat(60))); |
| 50 | + console.log(); |
| 51 | + |
| 52 | + // Sort by installs (descending) |
| 53 | + const sorted = [...skills].sort((a, b) => b.installs - a.installs); |
| 54 | + |
| 55 | + // Find codesyncer position |
| 56 | + const codesyncerIndex = sorted.findIndex(s => |
| 57 | + s.name.toLowerCase() === 'codesyncer' || |
| 58 | + s.topSource.includes('bitjaru/codesyncer') |
| 59 | + ); |
| 60 | + |
| 61 | + // Display top 10 |
| 62 | + const top10 = sorted.slice(0, 10); |
| 63 | + |
| 64 | + console.log(chalk.gray( |
| 65 | + ` ${isKo ? '순위' : 'Rank'} ${(isKo ? '이름' : 'Name').padEnd(25)} ${(isKo ? '설치수' : 'Installs').padStart(10)}` |
| 66 | + )); |
| 67 | + console.log(chalk.gray(' ' + '─'.repeat(45))); |
| 68 | + |
| 69 | + top10.forEach((skill, index) => { |
| 70 | + const rank = index + 1; |
| 71 | + const isCodesyncer = skill.name.toLowerCase() === 'codesyncer' || |
| 72 | + skill.topSource.includes('bitjaru/codesyncer'); |
| 73 | + |
| 74 | + const rankStr = rank <= 3 |
| 75 | + ? ['🥇', '🥈', '🥉'][rank - 1] |
| 76 | + : `${rank}.`.padStart(3); |
| 77 | + |
| 78 | + const nameStr = skill.name.padEnd(25); |
| 79 | + const installsStr = skill.installs.toLocaleString().padStart(10); |
| 80 | + |
| 81 | + if (isCodesyncer) { |
| 82 | + console.log(chalk.bold.green(` ${rankStr} ${nameStr} ${installsStr} ← You are here!`)); |
| 83 | + } else { |
| 84 | + console.log(chalk.white(` ${rankStr} ${nameStr} ${installsStr}`)); |
| 85 | + } |
| 86 | + }); |
| 87 | + |
| 88 | + // If codesyncer is not in top 10, show its position |
| 89 | + if (codesyncerIndex >= 10) { |
| 90 | + const codesyncer = sorted[codesyncerIndex]; |
| 91 | + console.log(chalk.gray(' ...')); |
| 92 | + console.log(chalk.bold.yellow( |
| 93 | + ` ${(codesyncerIndex + 1).toString().padStart(3)}. ${codesyncer.name.padEnd(25)} ${codesyncer.installs.toLocaleString().padStart(10)}` |
| 94 | + )); |
| 95 | + } |
| 96 | + |
| 97 | + console.log(); |
| 98 | + console.log(chalk.gray('─'.repeat(60))); |
| 99 | + console.log(chalk.gray( |
| 100 | + isKo |
| 101 | + ? ` 총 ${skills.length}개 스킬 | 데이터: skills.sh` |
| 102 | + : ` Total ${skills.length} skills | Data: skills.sh` |
| 103 | + )); |
| 104 | + console.log(); |
| 105 | + |
| 106 | + // Installation guide |
| 107 | + console.log(chalk.bold(isKo ? '📦 스킬 설치하기' : '📦 Install a Skill')); |
| 108 | + console.log(chalk.gray( |
| 109 | + isKo |
| 110 | + ? ' npx skills add <owner/repo>' |
| 111 | + : ' npx skills add <owner/repo>' |
| 112 | + )); |
| 113 | + console.log(); |
| 114 | + console.log(chalk.cyan(' npx skills add bitjaru/codesyncer')); |
| 115 | + console.log(); |
| 116 | +} |
| 117 | + |
| 118 | +/** |
| 119 | + * Install a skill using npx skills add |
| 120 | + */ |
| 121 | +async function installSkill(skillName: string, isKo: boolean): Promise<void> { |
| 122 | + const spinner = ora( |
| 123 | + isKo |
| 124 | + ? `${skillName} 설치 중...` |
| 125 | + : `Installing ${skillName}...` |
| 126 | + ).start(); |
| 127 | + |
| 128 | + return new Promise((resolve, reject) => { |
| 129 | + const child = spawn('npx', ['skills', 'add', skillName], { |
| 130 | + stdio: 'inherit', |
| 131 | + shell: true, |
| 132 | + }); |
| 133 | + |
| 134 | + spinner.stop(); |
| 135 | + |
| 136 | + child.on('close', (code) => { |
| 137 | + if (code === 0) { |
| 138 | + console.log(); |
| 139 | + console.log(chalk.green( |
| 140 | + isKo |
| 141 | + ? `✅ ${skillName} 설치 완료!` |
| 142 | + : `✅ ${skillName} installed successfully!` |
| 143 | + )); |
| 144 | + resolve(); |
| 145 | + } else { |
| 146 | + reject(new Error( |
| 147 | + isKo |
| 148 | + ? `설치 실패 (exit code: ${code})` |
| 149 | + : `Installation failed (exit code: ${code})` |
| 150 | + )); |
| 151 | + } |
| 152 | + }); |
| 153 | + |
| 154 | + child.on('error', (error) => { |
| 155 | + spinner.fail( |
| 156 | + isKo |
| 157 | + ? '설치 실패' |
| 158 | + : 'Installation failed' |
| 159 | + ); |
| 160 | + reject(error); |
| 161 | + }); |
| 162 | + }); |
| 163 | +} |
| 164 | + |
| 165 | +export interface SkillsOptions { |
| 166 | + // Reserved for future options |
| 167 | +} |
| 168 | + |
| 169 | +/** |
| 170 | + * Main skills command |
| 171 | + */ |
| 172 | +export async function skillsCommand(subcommand?: string, skillName?: string): Promise<void> { |
| 173 | + // Detect language from environment or default to English |
| 174 | + const isKo = process.env.LANG?.startsWith('ko') || false; |
| 175 | + |
| 176 | + console.log(chalk.bold.cyan('\n🎯 CodeSyncer - Skills\n')); |
| 177 | + |
| 178 | + // Handle subcommands |
| 179 | + if (subcommand === 'add') { |
| 180 | + if (!skillName) { |
| 181 | + console.log(chalk.red( |
| 182 | + isKo |
| 183 | + ? '❌ 스킬 이름을 지정해주세요' |
| 184 | + : '❌ Please specify a skill name' |
| 185 | + )); |
| 186 | + console.log(); |
| 187 | + console.log(chalk.gray(isKo ? '사용법:' : 'Usage:')); |
| 188 | + console.log(chalk.cyan(' codesyncer skills add <owner/repo>')); |
| 189 | + console.log(chalk.cyan(' codesyncer skills add bitjaru/codesyncer')); |
| 190 | + return; |
| 191 | + } |
| 192 | + |
| 193 | + try { |
| 194 | + await installSkill(skillName, isKo); |
| 195 | + } catch (error) { |
| 196 | + console.log(chalk.red( |
| 197 | + isKo |
| 198 | + ? `❌ 설치 실패: ${error instanceof Error ? error.message : 'Unknown error'}` |
| 199 | + : `❌ Installation failed: ${error instanceof Error ? error.message : 'Unknown error'}` |
| 200 | + )); |
| 201 | + } |
| 202 | + return; |
| 203 | + } |
| 204 | + |
| 205 | + // Default: show leaderboard |
| 206 | + const spinner = ora( |
| 207 | + isKo |
| 208 | + ? 'skills.sh에서 데이터 가져오는 중...' |
| 209 | + : 'Fetching data from skills.sh...' |
| 210 | + ).start(); |
| 211 | + |
| 212 | + try { |
| 213 | + const skills = await fetchSkills(); |
| 214 | + spinner.succeed( |
| 215 | + isKo |
| 216 | + ? '데이터 로드 완료' |
| 217 | + : 'Data loaded' |
| 218 | + ); |
| 219 | + displayLeaderboard(skills, isKo); |
| 220 | + } catch (error) { |
| 221 | + spinner.fail( |
| 222 | + isKo |
| 223 | + ? 'skills.sh 연결 실패' |
| 224 | + : 'Failed to connect to skills.sh' |
| 225 | + ); |
| 226 | + console.log(); |
| 227 | + console.log(chalk.red( |
| 228 | + error instanceof Error ? error.message : 'Unknown error' |
| 229 | + )); |
| 230 | + console.log(); |
| 231 | + console.log(chalk.gray( |
| 232 | + isKo |
| 233 | + ? '인터넷 연결을 확인하거나, 나중에 다시 시도해주세요.' |
| 234 | + : 'Check your internet connection or try again later.' |
| 235 | + )); |
| 236 | + } |
| 237 | +} |
0 commit comments