Skip to content

Instantly share code, notes, and snippets.

@rvanbaalen
Last active January 29, 2026 02:41
Show Gist options
  • Select an option

  • Save rvanbaalen/2e4c2840d06de810f771a4514f97c6da to your computer and use it in GitHub Desktop.

Select an option

Save rvanbaalen/2e4c2840d06de810f771a4514f97c6da to your computer and use it in GitHub Desktop.
Claude Code Skills Installer - Discover and install skills from GitHub Gists

Claude Code Skills Installer

Discover, install, and manage Claude Code skills from GitHub Gists.

Quick Start

Run the installer directly (no installation required):

curl -fsSL https://gist.githubusercontent.com/rvanbaalen/2e4c2840d06de810f771a4514f97c6da/raw/claude-skills.js -o /tmp/claude-skills.js && node /tmp/claude-skills.js

Features

  • Interactive TUI - Browse and select skills with keyboard navigation
  • Browse any user - Discover skills from any GitHub user's gists
  • Direct install - Install any skill directly from a gist ID
  • Direct uninstall - Remove skills by name from the command line
  • Auto-updates - Detects when installed skills have newer versions

Usage

Interactive Mode (TUI)

# Browse default skills
claude-skill-gists

# Browse skills from a specific user
claude-skill-gists anthropic

Navigate with arrow keys, select with Space, install with Enter.

List Skills

# List installed skills
claude-skill-gists list

# List available skills from a GitHub user
claude-skill-gists list octocat

Install from Gist ID

# Install using gist ID
claude-skill-gists install abc123def456

# Or using full gist URL
claude-skill-gists install https://gist.github.com/user/abc123def456

Uninstall Skills

# Uninstall by name
claude-skill-gists uninstall my-skill

# Interactive uninstall menu
claude-skill-gists uninstall

Other Commands

claude-skill-gists check     # Check for updates
claude-skill-gists update    # Update outdated skills
claude-skill-gists help      # Show help

Installing the Script Locally

Press s in the interactive TUI to install the script to /usr/local/bin/claude-skill-gists, then run it anytime with:

claude-skill-gists

Creating Your Own Skills

A skill is a GitHub Gist containing a SKILL.md file with YAML frontmatter:

---
name: my-skill
description: What this skill does and when Claude should use it
---

Instructions for Claude go here...

Requirements

  • Node.js 18+
  • GitHub CLI (gh) - authenticated with gh auth login
  • curl

License

MIT License - Created by Robin van Baalen

#!/usr/bin/env node
/**
* Claude Code Skills Installer
* Discovers, installs, and updates Claude Code skills from GitHub Gists
* Created by Robin van Baalen (https://robinvanbaalen.nl)
*/
import { execSync } from 'child_process';
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, readdirSync } from 'fs';
import { createHash } from 'crypto';
import { homedir } from 'os';
import { join } from 'path';
import { fileURLToPath } from 'url';
import * as readline from 'readline';
// ─────────────────────────────────────────────────────────────
// Self-update check
// ─────────────────────────────────────────────────────────────
const SCRIPT_GIST_URL = 'https://gist.githubusercontent.com/rvanbaalen/2e4c2840d06de810f771a4514f97c6da/raw/claude-skills.js';
async function checkForScriptUpdate() {
// Get current script path
const currentPath = fileURLToPath(import.meta.url);
// Skip if running from /tmp (one-time run)
if (currentPath.startsWith('/tmp') || currentPath.startsWith('/var')) {
return;
}
try {
const localContent = readFileSync(currentPath, 'utf-8');
const localHash = createHash('md5').update(localContent).digest('hex');
const remoteContent = execSync(`curl -fsSL "${SCRIPT_GIST_URL}"`, {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
});
const remoteHash = createHash('md5').update(remoteContent).digest('hex');
if (localHash !== remoteHash) {
console.log();
console.log(`${c.yellow}⚠${c.reset} A newer version of claude-skill-gists is available.`);
console.log();
const answer = await promptYesNo('Would you like to update now?');
if (answer) {
writeFileSync(currentPath, remoteContent);
console.log(`${c.green}✓${c.reset} Updated successfully. Please run the command again.`);
process.exit(0);
}
console.log();
}
} catch (e) {
// Silently ignore update check failures
}
}
async function promptYesNo(question) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) => {
rl.question(`${c.magenta}▸${c.reset} ${question} [y/N] `, (answer) => {
rl.close();
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
});
});
}
// ─────────────────────────────────────────────────────────────
// Config
// ─────────────────────────────────────────────────────────────
let githubUser = 'rvanbaalen';
const SKILLS_DIR = join(homedir(), '.claude', 'skills');
// ─────────────────────────────────────────────────────────────
// Colors
// ─────────────────────────────────────────────────────────────
const c = {
reset: '\x1b[0m',
bold: '\x1b[1m',
dim: '\x1b[2m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m',
white: '\x1b[37m',
bgBlue: '\x1b[44m',
bgGray: '\x1b[100m',
};
// ─────────────────────────────────────────────────────────────
// UI Helpers
// ─────────────────────────────────────────────────────────────
function clearScreen() {
process.stdout.write('\x1b[2J\x1b[H');
}
function hideCursor() {
process.stdout.write('\x1b[?25l');
}
function showCursor() {
process.stdout.write('\x1b[?25h');
}
function printHeader() {
console.log(`${c.cyan}`);
console.log(' ┌───────────────────────────────────────────────────────┐');
console.log(' │ │');
console.log(` │ ${c.bold}Claude Code Skills Installer${c.reset}${c.cyan} │`);
console.log(` │ ${c.dim}Discover and install skills from GitHub Gists${c.reset}${c.cyan} │`);
console.log(' │ │');
console.log(' └───────────────────────────────────────────────────────┘');
console.log(`${c.reset}`);
}
function printFooter() {
console.log();
console.log(`${c.dim}─────────────────────────────────────────────────────────${c.reset}`);
console.log(`${c.dim}Created by Robin van Baalen • robinvanbaalen.nl${c.reset}`);
console.log();
}
const log = {
info: (msg) => console.log(`${c.blue}ℹ${c.reset} ${msg}`),
success: (msg) => console.log(`${c.green}✓${c.reset} ${msg}`),
warn: (msg) => console.log(`${c.yellow}⚠${c.reset} ${msg}`),
error: (msg) => console.log(`${c.red}✗${c.reset} ${msg}`),
step: (msg) => console.log(`${c.magenta}▸${c.reset} ${msg}`),
};
// ─────────────────────────────────────────────────────────────
// Utilities
// ─────────────────────────────────────────────────────────────
function md5(content) {
return createHash('md5').update(content).digest('hex');
}
function ghApi(endpoint) {
try {
const result = execSync(`gh api "${endpoint}" --paginate`, {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
});
return JSON.parse(result);
} catch (e) {
return null;
}
}
function fetchUrl(url) {
try {
return execSync(`curl -fsSL "${url}"`, {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
});
} catch (e) {
return null;
}
}
function checkDependencies() {
const missing = [];
try {
execSync('gh --version', { stdio: 'pipe' });
} catch {
missing.push('gh (GitHub CLI)');
}
try {
execSync('curl --version', { stdio: 'pipe' });
} catch {
missing.push('curl');
}
if (missing.length > 0) {
log.error(`Missing dependencies: ${missing.join(', ')}`);
console.log();
log.info('Please install the missing dependencies and try again.');
process.exit(1);
}
// Check gh auth
try {
execSync('gh auth status', { stdio: 'pipe' });
} catch {
log.error('GitHub CLI is not authenticated');
console.log();
log.info(`Please run: ${c.cyan}gh auth login${c.reset}`);
process.exit(1);
}
}
function parseYamlFrontmatter(content) {
const lines = content.split('\n');
if (lines[0] !== '---') return null;
const endIndex = lines.indexOf('---', 1);
if (endIndex === -1) return null;
const frontmatter = {};
for (let i = 1; i < endIndex; i++) {
const match = lines[i].match(/^(\w+):\s*(.+)$/);
if (match) {
frontmatter[match[1]] = match[2].replace(/^["']|["']$/g, '');
}
}
return frontmatter;
}
// ─────────────────────────────────────────────────────────────
// Core Functions
// ─────────────────────────────────────────────────────────────
async function fetchSkills() {
log.step(`Fetching gists from ${c.cyan}@${githubUser}${c.reset}...`);
const gists = ghApi(`users/${githubUser}/gists`);
if (!gists) {
log.error(`Failed to fetch gists from @${githubUser}`);
process.exit(1);
}
const skills = [];
let updates = 0;
for (const gist of gists) {
const files = Object.keys(gist.files);
if (!files.includes('SKILL.md')) continue;
const rawUrl = gist.files['SKILL.md'].raw_url;
const content = fetchUrl(rawUrl);
if (!content) continue;
const frontmatter = parseYamlFrontmatter(content);
if (!frontmatter?.name) continue;
const name = frontmatter.name;
const description = frontmatter.description || 'No description';
const installPath = join(SKILLS_DIR, name, 'SKILL.md');
let status = 'new';
if (existsSync(installPath)) {
const localContent = readFileSync(installPath, 'utf-8');
if (md5(localContent) !== md5(content)) {
status = 'update';
updates++;
} else {
status = 'installed';
}
}
skills.push({
name,
description,
gistId: gist.id,
rawUrl,
content,
status,
selected: status === 'update',
});
}
if (skills.length === 0) {
log.warn(`No Claude Code skills found in @${githubUser}'s gists`);
process.exit(0);
}
log.success(`Found ${c.white}${c.bold}${skills.length}${c.reset} skill(s)`);
if (updates > 0) {
log.info(`${c.yellow}${updates}${c.reset} update(s) available`);
}
console.log();
return skills;
}
// ─────────────────────────────────────────────────────────────
// Direct Install from Gist ID
// ─────────────────────────────────────────────────────────────
async function installFromGistId(gistId) {
printHeader();
checkDependencies();
log.step(`Fetching gist ${c.cyan}${gistId}${c.reset}...`);
const gist = ghApi(`gists/${gistId}`);
if (!gist) {
log.error('Failed to fetch gist. Check if the gist ID is correct.');
process.exit(1);
}
const files = Object.keys(gist.files);
// Check for SKILL.md (case-insensitive)
const skillFile = files.find(f => f.toLowerCase() === 'skill.md');
if (!skillFile) {
log.error('This gist does not contain a SKILL.md file.');
console.log();
log.info('A valid Claude Code skill gist must contain a SKILL.md file.');
process.exit(1);
}
const rawUrl = gist.files[skillFile].raw_url;
const content = fetchUrl(rawUrl);
if (!content) {
log.error('Failed to fetch skill content.');
process.exit(1);
}
const frontmatter = parseYamlFrontmatter(content);
if (!frontmatter?.name) {
log.error('SKILL.md is missing required "name" field in frontmatter.');
console.log();
log.info('The skill file must have YAML frontmatter with at least a "name" field.');
process.exit(1);
}
const name = frontmatter.name;
const description = frontmatter.description || 'No description';
const installDir = join(SKILLS_DIR, name);
const installPath = join(installDir, 'SKILL.md');
// Check if already installed
let isUpdate = false;
if (existsSync(installPath)) {
const localContent = readFileSync(installPath, 'utf-8');
if (md5(localContent) === md5(content)) {
log.success(`Skill ${c.cyan}${name}${c.reset} is already installed and up to date.`);
printFooter();
return;
}
isUpdate = true;
}
console.log();
console.log(` ${c.bold}Skill:${c.reset} ${c.cyan}${name}${c.reset}`);
console.log(` ${c.bold}Description:${c.reset} ${c.dim}${description.slice(0, 50)}${c.reset}`);
console.log(` ${c.bold}Action:${c.reset} ${isUpdate ? 'Update' : 'Install'}`);
console.log();
const confirm = await promptYesNo(`${isUpdate ? 'Update' : 'Install'} this skill?`);
if (!confirm) {
console.log();
log.info('Installation cancelled.');
printFooter();
return;
}
try {
mkdirSync(installDir, { recursive: true });
if (existsSync(installPath)) {
writeFileSync(`${installPath}.bak`, readFileSync(installPath));
}
writeFileSync(installPath, content);
console.log();
if (isUpdate) {
log.success(`Updated ${c.cyan}${name}${c.reset}`);
} else {
log.success(`Installed ${c.cyan}${name}${c.reset}`);
}
console.log();
console.log(` ${c.dim}Location:${c.reset} ${installPath}`);
console.log();
console.log(`${c.bold}Usage:${c.reset}`);
console.log(` In Claude Code, run ${c.cyan}/${name}${c.reset} to invoke this skill`);
} catch (e) {
log.error(`Failed to install: ${e.message}`);
process.exit(1);
}
printFooter();
}
// ─────────────────────────────────────────────────────────────
// Direct Uninstall by Skill Name
// ─────────────────────────────────────────────────────────────
async function uninstallByName(skillName) {
printHeader();
const skillDir = join(SKILLS_DIR, skillName);
const skillPath = join(skillDir, 'SKILL.md');
if (!existsSync(skillPath)) {
log.error(`Skill ${c.cyan}${skillName}${c.reset} is not installed.`);
console.log();
log.info(`Run ${c.cyan}claude-skill-gists list${c.reset} to see installed skills.`);
printFooter();
process.exit(1);
}
// Read skill info for display
const content = readFileSync(skillPath, 'utf-8');
const frontmatter = parseYamlFrontmatter(content);
const description = frontmatter?.description || 'No description';
console.log(` ${c.bold}Skill:${c.reset} ${c.cyan}${skillName}${c.reset}`);
console.log(` ${c.bold}Description:${c.reset} ${c.dim}${description.slice(0, 50)}${c.reset}`);
console.log(` ${c.bold}Location:${c.reset} ${skillDir}`);
console.log();
const confirm = await promptYesNo(`Uninstall this skill?`);
if (!confirm) {
console.log();
log.info('Uninstall cancelled.');
printFooter();
return;
}
try {
rmSync(skillDir, { recursive: true, force: true });
console.log();
log.success(`Removed ${c.cyan}${skillName}${c.reset}`);
} catch (e) {
log.error(`Failed to uninstall: ${e.message}`);
process.exit(1);
}
printFooter();
}
// ─────────────────────────────────────────────────────────────
// List Skills from GitHub User
// ─────────────────────────────────────────────────────────────
async function listFromUser(username) {
clearScreen();
printHeader();
checkDependencies();
githubUser = username;
log.step(`Fetching skills from ${c.cyan}@${username}${c.reset}...`);
const gists = ghApi(`users/${username}/gists`);
if (!gists) {
log.error(`Failed to fetch gists from @${username}`);
console.log();
log.info('Check if the username is correct and the user has public gists.');
printFooter();
process.exit(1);
}
const skills = [];
for (const gist of gists) {
const files = Object.keys(gist.files);
if (!files.includes('SKILL.md')) continue;
const rawUrl = gist.files['SKILL.md'].raw_url;
const content = fetchUrl(rawUrl);
if (!content) continue;
const frontmatter = parseYamlFrontmatter(content);
if (!frontmatter?.name) continue;
const name = frontmatter.name;
const description = frontmatter.description || 'No description';
const installPath = join(SKILLS_DIR, name, 'SKILL.md');
let status = 'available';
if (existsSync(installPath)) {
const localContent = readFileSync(installPath, 'utf-8');
if (md5(localContent) !== md5(content)) {
status = 'update';
} else {
status = 'installed';
}
}
skills.push({ name, description, gistId: gist.id, status });
}
console.log();
console.log(`${c.bold}Available Skills from @${username}:${c.reset}`);
console.log();
if (skills.length === 0) {
log.warn(`No Claude Code skills found in @${username}'s gists`);
printFooter();
return;
}
for (const skill of skills) {
let statusLabel = '';
if (skill.status === 'installed') {
statusLabel = ` ${c.green}(installed)${c.reset}`;
} else if (skill.status === 'update') {
statusLabel = ` ${c.yellow}(update available)${c.reset}`;
}
console.log(` ${c.cyan}${skill.name}${c.reset}${statusLabel}`);
console.log(` ${c.dim}${skill.description.slice(0, 60)}${c.reset}`);
console.log(` ${c.dim}gist: ${skill.gistId}${c.reset}`);
console.log();
}
log.info(`To install: ${c.cyan}claude-skill-gists install <gist-id>${c.reset}`);
printFooter();
}
// ─────────────────────────────────────────────────────────────
// Interactive Selection UI
// ─────────────────────────────────────────────────────────────
async function interactiveSelect(skills, updateMode = false) {
// Filter skills for update mode
const displaySkills = updateMode
? skills.filter((s) => s.status === 'update')
: skills;
if (displaySkills.length === 0) {
log.success('All skills are up to date!');
return [];
}
let cursor = 0;
const selected = new Set(
displaySkills
.map((s, i) => (s.selected ? i : -1))
.filter((i) => i >= 0)
);
// Add "Install" button as last item
const INSTALL_BUTTON = displaySkills.length;
function render() {
clearScreen();
printHeader();
console.log(`${c.bold}Select skills to install:${c.reset} ${c.dim}(from @${githubUser})${c.reset}`);
console.log(`${c.dim}Use ↑↓ to navigate, Space to select, Enter to install${c.reset}`);
console.log();
displaySkills.forEach((skill, i) => {
const isHighlighted = cursor === i;
const isSelected = selected.has(i);
const checkbox = isSelected
? `${c.green}[✓]${c.reset}`
: `${c.dim}[ ]${c.reset}`;
let statusLabel = '';
if (skill.status === 'installed') {
statusLabel = ` ${c.green}(up to date)${c.reset}`;
} else if (skill.status === 'update') {
statusLabel = ` ${c.yellow}(update available)${c.reset}`;
}
const prefix = isHighlighted ? `${c.cyan}❯${c.reset}` : ' ';
const name = isHighlighted
? `${c.cyan}${c.bold}${skill.name}${c.reset}`
: `${c.white}${skill.name}${c.reset}`;
console.log(` ${prefix} ${checkbox} ${name}${statusLabel}`);
console.log(` ${c.dim}${skill.description.slice(0, 50)}${c.reset}`);
});
console.log();
// Install button
const isInstallHighlighted = cursor === INSTALL_BUTTON;
const selectedCount = selected.size;
const buttonText = selectedCount > 0
? `Install ${selectedCount} skill${selectedCount > 1 ? 's' : ''}`
: 'Install';
if (isInstallHighlighted) {
console.log(` ${c.cyan}❯${c.reset} ${c.bgBlue}${c.white}${c.bold} ${buttonText} ${c.reset}`);
} else {
console.log(` ${c.dim}[ ${buttonText} ]${c.reset}`);
}
console.log();
console.log(`${c.dim} a = select all · n = select none · g = change user${c.reset}`);
console.log(`${c.dim} s = install this script locally · q = quit${c.reset}`);
}
return new Promise((resolve) => {
hideCursor();
render();
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.setEncoding('utf8');
const cleanup = () => {
process.stdin.setRawMode(false);
process.stdin.pause();
showCursor();
};
process.stdin.on('data', async (key) => {
// Ctrl+C
if (key === '\^C') {
cleanup();
clearScreen();
log.info('Cancelled');
process.exit(0);
}
// q to quit
if (key === 'q' || key === 'Q') {
cleanup();
clearScreen();
log.info('Cancelled');
process.exit(0);
}
// Arrow up
if (key === '\^[[A') {
cursor = cursor > 0 ? cursor - 1 : INSTALL_BUTTON;
render();
return;
}
// Arrow down
if (key === '\^[[B') {
cursor = cursor < INSTALL_BUTTON ? cursor + 1 : 0;
render();
return;
}
// Space to toggle
if (key === ' ') {
if (cursor < displaySkills.length) {
if (selected.has(cursor)) {
selected.delete(cursor);
} else {
selected.add(cursor);
}
render();
}
return;
}
// Enter to confirm
if (key === '\r' || key === '\n') {
if (cursor === INSTALL_BUTTON || selected.size > 0) {
cleanup();
const result = displaySkills.filter((_, i) => selected.has(i));
resolve(result);
}
return;
}
// 'a' to select all
if (key === 'a' || key === 'A') {
for (let i = 0; i < displaySkills.length; i++) {
selected.add(i);
}
render();
return;
}
// 'n' to select none
if (key === 'n' || key === 'N') {
selected.clear();
render();
return;
}
// 'g' to change GitHub user
if (key === 'g' || key === 'G') {
cleanup();
const newUser = await promptForUsername();
if (newUser && newUser.trim()) {
githubUser = newUser.trim();
resolve({ changeUser: true });
} else {
// Re-enable raw mode and continue
process.stdin.setRawMode(true);
process.stdin.resume();
render();
}
return;
}
// 's' to install script locally
if (key === 's' || key === 'S') {
cleanup();
await installScriptLocally();
process.stdin.setRawMode(true);
process.stdin.resume();
render();
return;
}
});
});
}
async function promptForUsername() {
showCursor();
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) => {
rl.question(`${c.magenta}▸${c.reset} Enter GitHub username: `, (answer) => {
rl.close();
resolve(answer);
});
});
}
async function installScriptLocally() {
showCursor();
const installPath = '/usr/local/bin/claude-skill-gists';
const currentPath = fileURLToPath(import.meta.url);
// Check if already installed at target location
if (currentPath === installPath) {
console.log();
log.info('Already running from installed location.');
console.log();
await pressAnyKey();
return;
}
console.log();
console.log(`${c.bold}Install claude-skill-gists locally${c.reset}`);
console.log();
console.log(` ${c.dim}Location:${c.reset} ${installPath}`);
console.log();
const confirm = await promptYesNo('Proceed with installation?');
if (!confirm) {
console.log();
log.info('Installation cancelled.');
console.log();
await pressAnyKey();
return;
}
try {
// Fetch latest version from gist
const content = execSync(`curl -fsSL "${SCRIPT_GIST_URL}"`, {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
});
// Try to write directly, fall back to sudo
try {
writeFileSync(installPath, content, { mode: 0o755 });
} catch (e) {
// Need sudo
console.log();
log.info('Requesting sudo access to write to /usr/local/bin...');
execSync(`sudo tee "${installPath}" > /dev/null && sudo chmod +x "${installPath}"`, {
input: content,
stdio: ['pipe', 'inherit', 'inherit'],
});
}
console.log();
log.success(`Installed successfully!`);
console.log();
console.log(` ${c.dim}Run with:${c.reset} ${c.cyan}claude-skill-gists${c.reset}`);
console.log();
} catch (e) {
console.log();
log.error(`Installation failed: ${e.message}`);
console.log();
}
await pressAnyKey();
}
async function pressAnyKey() {
return new Promise((resolve) => {
console.log(`${c.dim}Press any key to continue...${c.reset}`);
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.once('data', () => {
process.stdin.setRawMode(false);
resolve();
});
});
}
function installSkills(skills) {
clearScreen();
printHeader();
console.log(`${c.bold}Installing skills...${c.reset}`);
console.log();
let installed = 0;
let updated = 0;
let failed = 0;
for (const skill of skills) {
const actionVerb = skill.status === 'update' ? 'Updating' : 'Installing';
log.step(`${actionVerb} ${c.cyan}${skill.name}${c.reset}...`);
const installDir = join(SKILLS_DIR, skill.name);
const installPath = join(installDir, 'SKILL.md');
try {
mkdirSync(installDir, { recursive: true });
if (existsSync(installPath)) {
writeFileSync(`${installPath}.bak`, readFileSync(installPath));
}
writeFileSync(installPath, skill.content);
if (skill.status === 'update') {
log.success(`Updated ${c.cyan}${skill.name}${c.reset}`);
updated++;
} else {
log.success(`Installed ${c.cyan}${skill.name}${c.reset}`);
installed++;
}
} catch (e) {
log.error(`Failed to install ${skill.name}`);
failed++;
}
}
console.log();
console.log(`${c.green}${c.bold}Complete!${c.reset}`);
console.log();
if (installed > 0) {
console.log(` ${c.dim}Installed:${c.reset} ${c.green}${installed}${c.reset} skill(s)`);
}
if (updated > 0) {
console.log(` ${c.dim}Updated:${c.reset} ${c.blue}${updated}${c.reset} skill(s)`);
}
if (failed > 0) {
console.log(` ${c.dim}Failed:${c.reset} ${c.red}${failed}${c.reset} skill(s)`);
}
console.log();
console.log(` ${c.dim}Skills directory:${c.reset} ${SKILLS_DIR}`);
console.log();
console.log(`${c.bold}Usage:${c.reset}`);
console.log(` In Claude Code, run ${c.cyan}/<skill-name>${c.reset} to invoke a skill`);
printFooter();
}
async function checkUpdates() {
clearScreen();
printHeader();
checkDependencies();
log.step('Checking for updates...');
const gists = ghApi(`users/${githubUser}/gists`);
if (!gists) {
log.error('Failed to fetch gists');
process.exit(1);
}
const updateList = [];
for (const gist of gists) {
const files = Object.keys(gist.files);
if (!files.includes('SKILL.md')) continue;
const rawUrl = gist.files['SKILL.md'].raw_url;
const content = fetchUrl(rawUrl);
if (!content) continue;
const frontmatter = parseYamlFrontmatter(content);
if (!frontmatter?.name) continue;
const installPath = join(SKILLS_DIR, frontmatter.name, 'SKILL.md');
if (existsSync(installPath)) {
const localContent = readFileSync(installPath, 'utf-8');
if (md5(localContent) !== md5(content)) {
updateList.push(frontmatter.name);
}
}
}
console.log();
if (updateList.length === 0) {
log.success('All installed skills are up to date!');
} else {
log.warn(`${updateList.length} update(s) available:`);
console.log();
for (const name of updateList) {
console.log(` ${c.dim}•${c.reset} ${c.cyan}${name}${c.reset}`);
}
console.log();
log.info(`Run ${c.cyan}claude-skill-gists update${c.reset} to update`);
}
printFooter();
}
function listInstalled() {
clearScreen();
printHeader();
console.log(`${c.bold}Installed Skills:${c.reset}`);
console.log();
let found = 0;
if (existsSync(SKILLS_DIR)) {
const dirs = readdirSync(SKILLS_DIR, { withFileTypes: true });
for (const dir of dirs) {
if (!dir.isDirectory()) continue;
const skillPath = join(SKILLS_DIR, dir.name, 'SKILL.md');
if (!existsSync(skillPath)) continue;
const content = readFileSync(skillPath, 'utf-8');
const frontmatter = parseYamlFrontmatter(content);
const description = frontmatter?.description || '';
console.log(` ${c.cyan}${dir.name}${c.reset}`);
console.log(` ${c.dim}${description.slice(0, 60)}${c.reset}`);
console.log();
found++;
}
}
if (found === 0) {
log.info('No skills installed');
console.log();
log.info(`Run ${c.cyan}claude-skill-gists${c.reset} to install skills`);
}
printFooter();
}
async function uninstallMenu() {
const installedSkills = [];
if (existsSync(SKILLS_DIR)) {
const dirs = readdirSync(SKILLS_DIR, { withFileTypes: true });
for (const dir of dirs) {
if (!dir.isDirectory()) continue;
const skillPath = join(SKILLS_DIR, dir.name, 'SKILL.md');
if (existsSync(skillPath)) {
const content = readFileSync(skillPath, 'utf-8');
const frontmatter = parseYamlFrontmatter(content);
installedSkills.push({
name: dir.name,
description: frontmatter?.description || '',
selected: false,
});
}
}
}
if (installedSkills.length === 0) {
clearScreen();
printHeader();
log.warn('No skills installed');
printFooter();
process.exit(0);
}
let cursor = 0;
const selected = new Set();
const UNINSTALL_BUTTON = installedSkills.length;
function render() {
clearScreen();
printHeader();
console.log(`${c.bold}Select skills to uninstall:${c.reset}`);
console.log(`${c.dim}Use ↑↓ to navigate, Space to select, Enter to uninstall, q to quit${c.reset}`);
console.log();
installedSkills.forEach((skill, i) => {
const isHighlighted = cursor === i;
const isSelected = selected.has(i);
const checkbox = isSelected
? `${c.red}[✓]${c.reset}`
: `${c.dim}[ ]${c.reset}`;
const prefix = isHighlighted ? `${c.cyan}❯${c.reset}` : ' ';
const name = isHighlighted
? `${c.cyan}${c.bold}${skill.name}${c.reset}`
: `${c.white}${skill.name}${c.reset}`;
console.log(` ${prefix} ${checkbox} ${name}`);
console.log(` ${c.dim}${skill.description.slice(0, 50)}${c.reset}`);
});
console.log();
const isButtonHighlighted = cursor === UNINSTALL_BUTTON;
const selectedCount = selected.size;
const buttonText = selectedCount > 0
? `Uninstall ${selectedCount} skill${selectedCount > 1 ? 's' : ''}`
: 'Uninstall';
if (isButtonHighlighted) {
console.log(` ${c.cyan}❯${c.reset} ${c.bgGray}${c.red}${c.bold} ${buttonText} ${c.reset}`);
} else {
console.log(` ${c.dim}[ ${buttonText} ]${c.reset}`);
}
console.log();
console.log(`${c.dim} a = select all · n = select none · q = quit${c.reset}`);
}
return new Promise((resolve) => {
hideCursor();
render();
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.setEncoding('utf8');
const cleanup = () => {
process.stdin.setRawMode(false);
process.stdin.pause();
showCursor();
};
process.stdin.on('data', async (key) => {
if (key === '\^C' || key === 'q' || key === 'Q') {
cleanup();
clearScreen();
log.info('Cancelled');
process.exit(0);
}
if (key === '\^[[A') {
cursor = cursor > 0 ? cursor - 1 : UNINSTALL_BUTTON;
render();
return;
}
if (key === '\^[[B') {
cursor = cursor < UNINSTALL_BUTTON ? cursor + 1 : 0;
render();
return;
}
if (key === ' ') {
if (cursor < installedSkills.length) {
if (selected.has(cursor)) {
selected.delete(cursor);
} else {
selected.add(cursor);
}
render();
}
return;
}
if (key === '\r' || key === '\n') {
if (cursor === UNINSTALL_BUTTON || selected.size > 0) {
cleanup();
clearScreen();
printHeader();
const toRemove = installedSkills.filter((_, i) => selected.has(i));
for (const skill of toRemove) {
rmSync(join(SKILLS_DIR, skill.name), { recursive: true, force: true });
log.success(`Removed ${c.cyan}${skill.name}${c.reset}`);
}
printFooter();
resolve();
}
return;
}
if (key === 'a' || key === 'A') {
for (let i = 0; i < installedSkills.length; i++) {
selected.add(i);
}
render();
return;
}
if (key === 'n' || key === 'N') {
selected.clear();
render();
return;
}
});
});
}
function showHelp() {
clearScreen();
printHeader();
console.log(`${c.bold}Usage:${c.reset}`);
console.log(' claude-skill-gists Interactive skill browser');
console.log(' claude-skill-gists <username> Browse skills from GitHub user');
console.log(' claude-skill-gists install <gist-id> Install skill from gist ID');
console.log(' claude-skill-gists uninstall <name> Uninstall skill by name');
console.log(' claude-skill-gists uninstall Interactive uninstall menu');
console.log(' claude-skill-gists update Update outdated skills');
console.log(' claude-skill-gists check Check for available updates');
console.log(' claude-skill-gists list List installed skills');
console.log(' claude-skill-gists list <username> List skills from GitHub user');
console.log(' claude-skill-gists help Show this help');
console.log();
console.log(`${c.bold}Examples:${c.reset}`);
console.log(` ${c.dim}# Browse skills from a user${c.reset}`);
console.log(` claude-skill-gists anthropic`);
console.log();
console.log(` ${c.dim}# List available skills from a user${c.reset}`);
console.log(` claude-skill-gists list octocat`);
console.log();
console.log(` ${c.dim}# Install a skill from any gist${c.reset}`);
console.log(` claude-skill-gists install abc123def456`);
console.log();
console.log(` ${c.dim}# Install from full gist URL${c.reset}`);
console.log(` claude-skill-gists install https://gist.github.com/user/abc123`);
console.log();
console.log(` ${c.dim}# Uninstall a specific skill${c.reset}`);
console.log(` claude-skill-gists uninstall my-skill`);
printFooter();
}
// ─────────────────────────────────────────────────────────────
// Main
// ─────────────────────────────────────────────────────────────
async function main() {
// Check for script updates (only if installed locally)
await checkForScriptUpdate();
const args = process.argv.slice(2);
const command = args[0];
// Handle subcommands
switch (command) {
case 'install': {
let gistId = args[1];
if (!gistId) {
log.error('Missing gist ID');
console.log();
log.info(`Usage: ${c.cyan}claude-skill-gists install <gist-id>${c.reset}`);
process.exit(1);
}
// Extract gist ID from URL if provided
const urlMatch = gistId.match(/gist\.github\.com\/[\w-]+\/([a-f0-9]+)/i);
if (urlMatch) {
gistId = urlMatch[1];
}
// Validate gist ID format (should be hex string)
if (!/^[a-f0-9]+$/i.test(gistId)) {
log.error('Invalid gist ID format');
console.log();
log.info('Gist ID should be a hexadecimal string (e.g., abc123def456)');
process.exit(1);
}
await installFromGistId(gistId);
break;
}
case 'uninstall': {
const skillName = args[1];
if (skillName) {
// Direct uninstall by name
await uninstallByName(skillName);
} else {
// Interactive uninstall menu
await uninstallMenu();
}
break;
}
case 'update':
clearScreen();
printHeader();
checkDependencies();
const skillsToUpdate = await fetchSkills();
const hasUpdates = skillsToUpdate.some((s) => s.status === 'update');
if (!hasUpdates) {
log.success('All skills are up to date!');
printFooter();
process.exit(0);
}
const toUpdate = await interactiveSelect(skillsToUpdate, true);
if (toUpdate.length > 0) {
installSkills(toUpdate);
}
break;
case 'check':
await checkUpdates();
break;
case 'list': {
const username = args[1];
if (username) {
// List skills from a specific GitHub user
await listFromUser(username);
} else {
// List installed skills
listInstalled();
}
break;
}
case 'help':
case '--help':
case '-h':
showHelp();
break;
// Legacy flag support
case '--update':
case '-U':
clearScreen();
printHeader();
checkDependencies();
const legacySkillsToUpdate = await fetchSkills();
const legacyHasUpdates = legacySkillsToUpdate.some((s) => s.status === 'update');
if (!legacyHasUpdates) {
log.success('All skills are up to date!');
printFooter();
process.exit(0);
}
const legacyToUpdate = await interactiveSelect(legacySkillsToUpdate, true);
if (legacyToUpdate.length > 0) {
installSkills(legacyToUpdate);
}
break;
case '--check':
case '-c':
await checkUpdates();
break;
case '--list':
case '-l':
listInstalled();
break;
case '--uninstall':
case '-u':
await uninstallMenu();
break;
default:
// Check if command looks like a username (no special chars, not a flag)
if (command && !command.startsWith('-') && /^[\w-]+$/.test(command)) {
// Treat as username - open interactive browser for that user
githubUser = command;
clearScreen();
printHeader();
checkDependencies();
let continueLoop = true;
while (continueLoop) {
const skills = await fetchSkills();
const result = await interactiveSelect(skills);
if (result && result.changeUser) {
clearScreen();
printHeader();
continue;
}
if (Array.isArray(result) && result.length > 0) {
installSkills(result);
}
continueLoop = false;
}
} else if (!command) {
// No command - default interactive mode
clearScreen();
printHeader();
checkDependencies();
let continueLoop = true;
while (continueLoop) {
const skills = await fetchSkills();
const result = await interactiveSelect(skills);
if (result && result.changeUser) {
clearScreen();
printHeader();
continue;
}
if (Array.isArray(result) && result.length > 0) {
installSkills(result);
}
continueLoop = false;
}
} else {
log.error(`Unknown command: ${command}`);
console.log();
log.info(`Run ${c.cyan}claude-skill-gists help${c.reset} for usage`);
process.exit(1);
}
break;
}
}
main().catch((e) => {
showCursor();
log.error(e.message);
process.exit(1);
});
{
"name": "claude-skills",
"version": "1.0.0",
"description": "Discover, install, and update Claude Code skills from GitHub Gists",
"type": "module",
"bin": {
"claude-skills": "./claude-skills.mjs"
},
"scripts": {
"start": "node claude-skills.mjs"
},
"keywords": [
"claude",
"claude-code",
"skills",
"installer",
"cli"
],
"author": "Robin van Baalen <robin@robinvanbaalen.nl> (https://robinvanbaalen.nl)",
"license": "MIT",
"engines": {
"node": ">=18"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment