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

239
apps/cli/src/index.ts Normal file
View File

@@ -0,0 +1,239 @@
#!/usr/bin/env node
import { Command } from 'commander';
import chalk from 'chalk';
import { createRequire } from 'module';
import { install } from './commands/install.js';
import { search } from './commands/search.js';
import { list } from './commands/list.js';
import { config } from './commands/config.js';
import { loadConfig, type Platform } from './utils/paths.js';
const require = createRequire(import.meta.url);
const pkg = require('../package.json');
const VERSION = pkg.version;
const program = new Command();
program
.name('skillhub')
.description('CLI for managing AI Agent skills')
.version(VERSION);
// Install command
program
.command('install <skill-id>')
.description('Install a skill from the registry')
.option('-p, --platform <platform>', 'Target platform (claude, codex, copilot, cursor, windsurf)')
.option('--project', 'Install in the current project instead of globally')
.option('-f, --force', 'Overwrite existing skill')
.option('--no-api', 'Skip API lookup and fetch directly from GitHub')
.action(async (skillId: string, options) => {
// Load config to get default platform
const userConfig = await loadConfig();
const platform = options.platform || (userConfig.defaultPlatform as Platform) || 'claude';
await install(skillId, {
platform,
project: options.project,
force: options.force,
noApi: !options.api, // Commander converts --no-api to api: false
});
});
// Search command
program
.command('search <query>')
.description('Search for skills in the registry')
.option('-p, --platform <platform>', 'Filter by platform')
.option('-s, --sort <sort>', 'Sort by: downloads, stars, rating, recent', 'downloads')
.option('-l, --limit <number>', 'Number of results', '10')
.option('--page <number>', 'Page number', '1')
.action(async (query: string, options) => {
await search(query, options);
});
// List command
program
.command('list')
.description('List installed skills')
.option('-p, --platform <platform>', 'Filter by platform')
.option('--project', 'List skills in the current project')
.option('--all', 'List both global and project skills')
.action(async (options) => {
await list(options);
});
// Config command
program
.command('config')
.description('Manage CLI configuration')
.option('--set <key=value>', 'Set a configuration value')
.option('--get <key>', 'Get a configuration value')
.option('--list', 'List all configuration values')
.action(async (options) => {
await config(options);
});
// Uninstall command
program
.command('uninstall <skill-name>')
.description('Uninstall a skill')
.option('-p, --platform <platform>', 'Target platform')
.option('--project', 'Uninstall from project instead of globally')
.action(async (skillName: string, options) => {
const fs = await import('fs-extra');
const pathModule = await import('path');
const { getSkillPath, isSkillInstalled } = await import('./utils/paths.js');
// Load config to get default platform
const userConfig = await loadConfig();
const platform = options.platform || (userConfig.defaultPlatform as Platform) || 'claude';
const installed = await isSkillInstalled(platform, skillName, options.project);
if (!installed) {
console.log(chalk.yellow(`Skill ${skillName} is not installed.`));
process.exit(1);
}
const skillPath = getSkillPath(platform, skillName, options.project);
// Clean up flat platform file if present
const metadataPath = pathModule.join(skillPath, '.skillhub.json');
if (await fs.default.pathExists(metadataPath)) {
try {
const metadata = await fs.default.readJson(metadataPath);
if (metadata.platformFilePath) {
await fs.default.remove(metadata.platformFilePath);
}
} catch {
// Ignore metadata read errors
}
}
await fs.default.remove(skillPath);
console.log(chalk.green(`Skill ${skillName} uninstalled successfully.`));
});
// Update command
program
.command('update [skill-name]')
.description('Update installed skills')
.option('-p, --platform <platform>', 'Target platform')
.option('--all', 'Update all installed skills')
.action(async (skillName: string | undefined, options) => {
const fsExtra = await import('fs-extra');
const pathModule = await import('path');
const { getSkillsPath, getSkillPath } = await import('./utils/paths.js');
// Load config to get default platform
const userConfig = await loadConfig();
const platform = options.platform || (userConfig.defaultPlatform as Platform) || 'claude';
const ALL_PLATFORMS: Platform[] = ['claude', 'codex', 'copilot', 'cursor', 'windsurf'];
if (options.all) {
console.log(chalk.cyan('\nUpdating all installed skills...\n'));
const platforms = options.platform ? [platform] : ALL_PLATFORMS;
let updated = 0;
let failed = 0;
let skipped = 0;
for (const p of platforms) {
const skillsPath = getSkillsPath(p);
if (!(await fsExtra.default.pathExists(skillsPath))) {
continue;
}
const entries = await fsExtra.default.readdir(skillsPath, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const skillPath = pathModule.join(skillsPath, entry.name);
const metadataPath = pathModule.join(skillPath, '.skillhub.json');
// Check if skill has metadata with skillId
if (!(await fsExtra.default.pathExists(metadataPath))) {
console.log(chalk.yellow(` Skipping ${entry.name}: No metadata (reinstall with CLI to enable updates)`));
skipped++;
continue;
}
try {
const metadata = await fsExtra.default.readJson(metadataPath);
const skillId = metadata.skillId;
if (!skillId) {
console.log(chalk.yellow(` Skipping ${entry.name}: No skill ID in metadata`));
skipped++;
continue;
}
console.log(chalk.dim(` Updating ${entry.name}...`));
// Re-install with force
await install(skillId, {
platform: p,
force: true,
});
updated++;
} catch (error) {
console.log(chalk.red(` Failed to update ${entry.name}: ${(error as Error).message}`));
failed++;
}
}
}
console.log();
console.log(chalk.green(`Updated: ${updated}`));
if (failed > 0) console.log(chalk.red(`Failed: ${failed}`));
if (skipped > 0) console.log(chalk.yellow(`Skipped: ${skipped}`));
return;
}
if (!skillName) {
console.log(chalk.red('Please specify a skill name or use --all.'));
process.exit(1);
}
// Update single skill by name
const skillPath = getSkillPath(platform, skillName);
const metadataPath = pathModule.join(skillPath, '.skillhub.json');
if (!(await fsExtra.default.pathExists(metadataPath))) {
console.log(chalk.yellow(`Skill ${skillName} was not installed via CLI.`));
console.log(chalk.dim('To update, reinstall with: npx skillhub install <skill-id> --force'));
process.exit(1);
}
try {
const metadata = await fsExtra.default.readJson(metadataPath);
const skillId = metadata.skillId;
if (!skillId) {
console.log(chalk.red('No skill ID found in metadata.'));
process.exit(1);
}
console.log(chalk.dim(`Updating ${skillName}...`));
await install(skillId, {
platform,
force: true,
});
} catch (error) {
console.log(chalk.red(`Failed to update: ${(error as Error).message}`));
process.exit(1);
}
});
// Parse arguments
program.parse();
// Show help if no command
if (!process.argv.slice(2).length) {
program.help();
}