Initial release v1.0.0

Open-source marketplace for AI Agent skills.

Features:
- Next.js 15 web app with i18n (en/fa)
- CLI tool for skill installation (npx skillhub)
- GitHub crawler/indexer with multi-strategy discovery
- Security scanning for all indexed skills
- Self-hostable with Docker

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
airano
2026-02-12 06:08:51 +03:30
commit 97b427831a
227 changed files with 48411 additions and 0 deletions

View File

@@ -0,0 +1,228 @@
import { chromium } from 'playwright';
import { join } from 'path';
import { mkdirSync, existsSync, unlinkSync, readdirSync } from 'fs';
const SCREENSHOTS_DIR = './screenshots/cli-demo';
// Ensure directory exists
if (!existsSync(SCREENSHOTS_DIR)) {
mkdirSync(SCREENSHOTS_DIR, { recursive: true });
}
function generateTerminalHTML(prompt, output, status, showCursor) {
if (showCursor === undefined) showCursor = true;
const cursorHtml = showCursor ? '<span class="cursor"></span>' : '';
return `
<!DOCTYPE html>
<html>
<head>
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap');
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 800px;
height: 500px;
background: #0d1117;
font-family: 'JetBrains Mono', 'Consolas', monospace;
padding: 0;
display: flex;
flex-direction: column;
}
.window {
background: #161b22;
border-radius: 12px;
overflow: hidden;
border: 1px solid #30363d;
margin: 20px;
flex: 1;
display: flex;
flex-direction: column;
}
.titlebar {
background: #21262d;
padding: 12px 16px;
display: flex;
align-items: center;
gap: 8px;
border-bottom: 1px solid #30363d;
}
.dot {
width: 12px;
height: 12px;
border-radius: 50%;
}
.dot.red { background: #ff5f56; }
.dot.yellow { background: #ffbd2e; }
.dot.green { background: #27c93f; }
.title {
flex: 1;
text-align: center;
color: #8b949e;
font-size: 13px;
font-weight: 500;
}
.terminal {
padding: 20px;
font-size: 14px;
line-height: 1.6;
color: #c9d1d9;
flex: 1;
overflow: hidden;
}
.prompt { color: #7ee787; }
.command { color: #79c0ff; }
.success { color: #7ee787; }
.muted { color: #8b949e; }
.skill-id { color: #79c0ff; font-weight: 500; }
.stars { color: #f7c150; }
.downloads { color: #a5d6ff; }
.desc { color: #8b949e; }
.divider { color: #30363d; }
.cursor {
display: inline-block;
width: 8px;
height: 18px;
background: #c9d1d9;
animation: blink 1s step-end infinite;
vertical-align: text-bottom;
}
@keyframes blink {
50% { opacity: 0; }
}
.status {
padding: 8px 16px;
background: #21262d;
color: #8b949e;
font-size: 12px;
border-top: 1px solid #30363d;
}
.logo {
position: absolute;
bottom: 30px;
right: 30px;
font-size: 24px;
font-weight: 700;
background: linear-gradient(135deg, #66b3e6 0%, #f7c150 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
</style>
</head>
<body>
<div class="window">
<div class="titlebar">
<div class="dot red"></div>
<div class="dot yellow"></div>
<div class="dot green"></div>
<div class="title">Terminal — skillhub</div>
</div>
<div class="terminal">
<span class="prompt">${prompt}</span>${cursorHtml}
${output}
</div>
<div class="status">${status}</div>
</div>
<div class="logo">SkillHub</div>
</body>
</html>
`;
}
// CLI Demo frames - 4 distinct frames
const FRAMES = [
// Frame 1: Empty prompt, ready to type
{
content: generateTerminalHTML('$ ', '', 'Ready'),
description: 'Empty prompt'
},
// Frame 2: Command typed
{
content: generateTerminalHTML('$ npx skillhub search pdf', '', 'Press Enter to search...'),
description: 'Command typed'
},
// Frame 3: Searching animation
{
content: generateTerminalHTML('$ npx skillhub search pdf', `
<span class="muted">⠋ Searching skills...</span>
`, 'Searching...', false),
description: 'Searching'
},
// Frame 4: Results displayed (final state)
{
content: generateTerminalHTML('$ npx skillhub search pdf', `
<span class="success">✔ Found 156 skills:</span>
<span class="divider">─────────────────────────────────────────────────────────────────</span>
<span class="skill-id">anthropics/skills/pdf</span> <span class="stars">⭐ 30.2k</span> <span class="downloads">⬇ 1.2k</span>
<span class="desc">Read, extract and manipulate PDF files. Convert PDFs...</span>
<span class="divider">─────────────────────────────────────────────────────────────────</span>
<span class="skill-id">obra/superpowers/pdf-extraction</span> <span class="stars">⭐ 8.1k</span> <span class="downloads">⬇ 421</span>
<span class="desc">Advanced PDF extraction with OCR support and table...</span>
<span class="divider">─────────────────────────────────────────────────────────────────</span>
<span class="skill-id">openai/skills/pdf-reader</span> <span class="stars">⭐ 2.2k</span> <span class="downloads">⬇ 189</span>
<span class="desc">Parse PDF documents and extract structured text...</span>
<span class="divider">─────────────────────────────────────────────────────────────────</span>
<span class="muted">Install with:</span> <span class="command">npx skillhub install &lt;skill-id&gt;</span>
<span class="muted">Showing 3 of 156. Use --limit to see more.</span>
`, 'Done - 156 skills found', false),
description: 'Results'
},
];
async function main() {
console.log('=== CLI Demo Animation Generator ===\n');
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
viewport: { width: 800, height: 500 },
deviceScaleFactor: 2,
});
const page = await context.newPage();
// Clean up old frames
const existingFiles = readdirSync(SCREENSHOTS_DIR);
for (const file of existingFiles) {
if (file.endsWith('.png')) {
unlinkSync(join(SCREENSHOTS_DIR, file));
}
}
console.log('Generating 4 distinct frames...');
for (let i = 0; i < FRAMES.length; i++) {
const frame = FRAMES[i];
await page.setContent(frame.content);
await page.screenshot({
path: join(SCREENSHOTS_DIR, 'frame-' + String(i).padStart(3, '0') + '.png'),
});
console.log(' Frame ' + (i + 1) + '/' + FRAMES.length + ': ' + frame.description);
}
await browser.close();
console.log('\n=== Frames Generated ===');
console.log('\nFrames saved to: ' + SCREENSHOTS_DIR + '/');
console.log('\nTo create GIF:');
console.log(' ffmpeg -framerate 0.5 -i frame-%03d.png -vf "fps=10,scale=800:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" -loop 0 ../cli-demo.gif');
}
main().catch(console.error);