UNPKG

skills-ws

Version:

87 agent skills for AI coding assistants — marketing, growth, web3, dev, design & operations. Built for OpenClaw, Claude Code, Cursor, and Codex.

416 lines (354 loc) • 14.9 kB
#!/usr/bin/env node import { readdir, readFile, copyFile, mkdir, stat } from "node:fs/promises"; import { homedir } from "node:os"; import { join, dirname } from "node:path"; import readline from "node:readline"; import { fileURLToPath } from "node:url"; const __dirname = dirname(fileURLToPath(import.meta.url)); const SKILLS_DIR = join(__dirname, "..", "skills"); const R = "\x1b[0m"; const B = "\x1b[1m"; const DIM = "\x1b[2m"; const GREEN = "\x1b[32m"; const CYAN = "\x1b[36m"; const YELLOW = "\x1b[33m"; const WHITE = "\x1b[37m"; const GRAY = "\x1b[90m"; // ── Banner ─────────────────────────────────────────────────── const LOGO = [ "███████╗██╗ ██╗██╗██╗ ██╗ ███████╗ ██╗ ██╗███████╗", "██╔════╝██║ ██╔╝██║██║ ██║ ██╔════╝ ██║ ██║██╔════╝", "███████╗█████╔╝ ██║██║ ██║ ███████╗ ██║ █╗ ██║███████╗", "╚════██║██╔═██╗ ██║██║ ██║ ╚════██║ ██║███╗██║╚════██║", "███████║██║ ██╗██║███████╗███████╗███████║ ╚███╔███╔╝███████║", "╚══════╝╚═╝ ╚═╝╚═╝╚══════╝╚══════╝╚══════╝ ╚══╝╚══╝ ╚══════╝", ]; // Color roles (ANSI) — semantic, not literal const C_BLOCK = "\x1b[36m"; // cyan — filled blocks ███ const C_SHADOW = "\x1b[90m"; // gray — shadow characters ╔═╗║╚╝ function colorizeLine(line) { let out = ""; for (const ch of line) { if (ch === "█") { out += C_BLOCK + ch; } else if (ch === " " || ch === "\n") { out += R + ch; } else { out += C_SHADOW + ch; } } return out + R; } function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); } function hideCursor() { process.stdout.write("\x1b[?25l"); } function showCursor() { process.stdout.write("\x1b[?25h"); } function moveTo(row, col) { process.stdout.write(`\x1b[${row};${col}H`); } function clearScreen() { process.stdout.write("\x1b[2J\x1b[H"); } async function playBanner() { if (!process.stdout.isTTY || process.env.NO_COLOR) { const pad = " "; process.stdout.write("\n"); for (const line of LOGO) { process.stdout.write(pad + colorizeLine(line) + "\n"); } process.stdout.write(`\n${pad}${DIM}agent skills for AI${R}\n\n`); return; } hideCursor(); clearScreen(); const cols = process.stdout.columns || 80; const rows = process.stdout.rows || 24; const logoWidth = Math.max(...LOGO.map(l => [...l].length)); const padLeft = Math.max(2, Math.floor((cols - logoWidth) / 2)); const padTop = Math.max(1, Math.floor((rows - LOGO.length - 4) / 2)); const pad = " ".repeat(padLeft); // Phase 1: Scanline reveal — draw line by line, char by char in groups for (let row = 0; row < LOGO.length; row++) { const line = LOGO[row]; const chars = [...line]; moveTo(padTop + row, 1); // Reveal in chunks of ~8 chars for speed const chunkSize = 6; for (let i = 0; i < chars.length; i += chunkSize) { const chunk = chars.slice(i, i + chunkSize).join(""); process.stdout.write(i === 0 ? pad : ""); // First pass: bright white flash process.stdout.write(`${B}${WHITE}${chunk}${R}`); await sleep(8); // Overwrite with final color const colPos = padLeft + i + 1; moveTo(padTop + row, colPos); process.stdout.write(colorizeLine(chunk)); } await sleep(30); } await sleep(150); // Phase 2: Pulse effect — flash blocks bright then back for (let pulse = 0; pulse < 2; pulse++) { // Flash bright for (let row = 0; row < LOGO.length; row++) { moveTo(padTop + row, 1); process.stdout.write(pad); for (const ch of LOGO[row]) { if (ch === "█") { process.stdout.write(`${B}${WHITE}${ch}${R}`); } else if (ch === " ") { process.stdout.write(" "); } else { process.stdout.write(`${C_SHADOW}${ch}${R}`); } } } await sleep(60); // Back to normal for (let row = 0; row < LOGO.length; row++) { moveTo(padTop + row, 1); process.stdout.write(pad + colorizeLine(LOGO[row])); } await sleep(80); } // Phase 3: Tagline fade-in const tagline = "agent skills for AI"; const tagPad = " ".repeat(Math.max(2, Math.floor((cols - tagline.length) / 2))); const tagRow = padTop + LOGO.length + 1; // Char by char for (let i = 0; i < tagline.length; i++) { moveTo(tagRow, tagPad.length + i + 1); process.stdout.write(`${GREEN}${tagline[i]}${R}`); await sleep(25); } // Underline const underline = "─".repeat(tagline.length); moveTo(tagRow + 1, tagPad.length + 1); for (let i = 0; i < underline.length; i++) { process.stdout.write(`${GRAY}─${R}`); await sleep(10); } await sleep(400); // Phase 4: Settle — clear and redraw static final version clearScreen(); process.stdout.write("\n"); for (const line of LOGO) { process.stdout.write(pad + colorizeLine(line) + "\n"); } process.stdout.write(`\n${tagPad}${DIM}${tagline}${R}\n\n`); showCursor(); } // ── Install Animation ──────────────────────────────────────── const PROGRESS_CHARS = "━"; const PROGRESS_EMPTY = "╌"; const PROGRESS_WIDTH = 30; async function playInstallProgress(skillCount) { if (!process.stdout.isTTY || process.env.NO_COLOR) { process.stdout.write(` ${GREEN}${skillCount} skill${skillCount !== 1 ? "s" : ""} installed${R}\n`); return; } hideCursor(); const labels = [ "reading manifests", "resolving skills", "copying files", "writing to disk", "verifying", ]; for (let i = 0; i <= PROGRESS_WIDTH; i++) { const filled = PROGRESS_CHARS.repeat(i); const empty = PROGRESS_EMPTY.repeat(PROGRESS_WIDTH - i); const pct = Math.round((i / PROGRESS_WIDTH) * 100); const label = labels[Math.min(Math.floor((i / PROGRESS_WIDTH) * labels.length), labels.length - 1)]; readline.cursorTo(process.stdout, 0); readline.clearLine(process.stdout, 0); process.stdout.write( ` ${CYAN}${filled}${GRAY}${empty}${R} ${DIM}${String(pct).padStart(3)}%${R} ${DIM}${label}${R}` ); await sleep(25); } readline.cursorTo(process.stdout, 0); readline.clearLine(process.stdout, 0); process.stdout.write( ` ${GREEN}${"━".repeat(PROGRESS_WIDTH)}${R} ${B}${skillCount} skill${skillCount !== 1 ? "s" : ""} installed${R}\n` ); showCursor(); } // ── Skill Operations ───────────────────────────────────────── async function getSkills() { const entries = await readdir(SKILLS_DIR, { withFileTypes: true }); const skills = []; for (const e of entries) { if (!e.isDirectory()) continue; const skillDir = join(SKILLS_DIR, e.name); const skillMd = join(skillDir, "SKILL.md"); try { await stat(skillMd); const content = await readFile(skillMd, "utf8"); const descMatch = content.match(/^description:\s*["']?(.+?)["']?\s*$/m); const desc = descMatch ? descMatch[1] : ""; skills.push({ name: e.name, desc, dir: skillDir }); } catch {} } return skills.sort((a, b) => a.name.localeCompare(b.name)); } async function copyDir(src, dest) { await mkdir(dest, { recursive: true }); const entries = await readdir(src, { withFileTypes: true }); for (const e of entries) { const srcPath = join(src, e.name); const destPath = join(dest, e.name); if (e.isDirectory()) await copyDir(srcPath, destPath); else await copyFile(srcPath, destPath); } } function targetCandidates() { const home = homedir(); if (!home) { process.stderr.write(` ${YELLOW}Error:${R} cannot resolve your home directory.\n`); process.exit(1); } const cwd = process.cwd(); // Paths each tool actually reads, per official docs (2026): // Claude Code: .claude/skills + ~/.claude/skills // Cursor: .cursor/skills, .agents/skills, ~/.cursor/skills, ~/.agents/skills // Codex: .agents/skills + ~/.agents/skills // OpenClaw: <workspace>/skills, <workspace>/.agents/skills, ~/.agents/skills, ~/.openclaw/skills return [ { path: join(cwd, ".claude", "skills"), agent: "Claude Code (project)" }, { path: join(cwd, ".cursor", "skills"), agent: "Cursor (project)" }, { path: join(cwd, ".agents", "skills"), agent: "Codex/Cursor/OpenClaw (project)" }, { path: join(cwd, "skills"), agent: "OpenClaw workspace" }, { path: join(home, ".claude", "skills"), agent: "Claude Code (user)" }, { path: join(home, ".cursor", "skills"), agent: "Cursor (user)" }, { path: join(home, ".agents", "skills"), agent: "Codex/Cursor/Copilot/OpenClaw (user)" }, { path: join(home, ".openclaw", "skills"), agent: "OpenClaw (global)" }, ]; } async function detectTarget() { const candidates = targetCandidates(); for (const c of candidates) { try { await stat(c.path); process.stdout.write(` ${DIM}detected: ${c.agent}${R}\n`); return c.path; } catch {} } // Default: ~/.agents/skills, the cross-tool path read by Cursor, Codex, // Copilot, and OpenClaw. Claude Code users will already have ~/.claude/skills. const fallback = join(homedir(), ".agents", "skills"); process.stdout.write(` ${DIM}no agent skills directory found, using cross-tool default${R}\n`); await mkdir(fallback, { recursive: true }); return fallback; } // ── Interactive Picker ─────────────────────────────────────── async function interactivePick(skills) { for (let i = 0; i < skills.length; i++) { process.stdout.write( ` ${GRAY}${String(i + 1).padStart(2)}${R} ${GREEN}${skills[i].name.padEnd(24)}${R}${DIM}${skills[i].desc.slice(0, 50)}${R}\n` ); } process.stdout.write(`\n ${YELLOW}Enter numbers or names (comma-separated), or 'all':${R}\n`); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const answer = await new Promise((resolve) => rl.question(` ${CYAN}> ${R}`, resolve)); rl.close(); const names = []; if (answer.trim().toLowerCase() === "all") { names.push(...skills.map((s) => s.name)); } else { for (const part of answer.split(",").map((s) => s.trim()).filter(Boolean)) { const num = parseInt(part); if (!isNaN(num) && num >= 1 && num <= skills.length) names.push(skills[num - 1].name); else names.push(part); } } return names; } async function installSkills(names, skills, customDir) { if (names.length === 0) { process.stdout.write(` ${DIM}Nothing selected.${R}\n`); return; } const target = customDir || await detectTarget(); process.stdout.write(`\n ${DIM}${target}${R}\n\n`); let installed = 0; for (const name of names) { const skill = skills.find((s) => s.name === name); if (!skill) { process.stdout.write(` ${YELLOW}skip${R} ${name} ${DIM}(not found)${R}\n`); continue; } await copyDir(skill.dir, join(target, name)); installed++; } await playInstallProgress(installed); process.stdout.write("\n"); for (const name of names) { if (skills.find((s) => s.name === name)) { process.stdout.write(` ${GREEN}+${R} ${name}\n`); } } process.stdout.write(`\n ${DIM}skills.ws${R}\n\n`); } // ── Main ───────────────────────────────────────────────────── async function main() { const rawArgs = process.argv.slice(2); const skills = await getSkills(); // Parse --dir flag from anywhere in args let customDir = null; const args = []; for (let i = 0; i < rawArgs.length; i++) { if (rawArgs[i] === "--dir" && i + 1 < rawArgs.length) { const dir = rawArgs[i + 1]; // Reject absolute paths and path traversal if (dir.startsWith("/") || dir.includes("..")) { process.stderr.write(` ${YELLOW}Error:${R} --dir must be a relative path without '..' components.\n`); process.exit(1); } customDir = dir; i++; // skip next } else { args.push(rawArgs[i]); } } await playBanner(); if (args[0] === "list" || args[0] === "ls") { for (const s of skills) { process.stdout.write(` ${GREEN}${s.name.padEnd(24)}${R}${DIM}${s.desc.slice(0, 55)}${R}\n`); } process.stdout.write(`\n ${DIM}${skills.length} skills | npx skills-ws install <name>${R}\n\n`); return; } if (args[0] === "install" || args[0] === "add" || args[0] === "--skill") { let names = args.slice(1); // Handle "all" keyword if (names.includes("all")) { names = skills.map((s) => s.name); } else if (names.length === 0) { names = await interactivePick(skills); } await installSkills(names, skills, customDir); return; } if (args[0] === "help" || args[0] === "-h" || args[0] === "--help") { process.stdout.write(` ${B}Usage:${R}\n\n`); process.stdout.write(` ${CYAN}npx skills-ws${R} Interactive picker\n`); process.stdout.write(` ${CYAN}npx skills-ws list${R} List all skills\n`); process.stdout.write(` ${CYAN}npx skills-ws install <name>${R} Install specific skill(s)\n`); process.stdout.write(` ${CYAN}npx skills-ws install all${R} Install everything\n`); process.stdout.write(` ${CYAN}npx skills-ws install --dir .${R} Install to custom directory\n`); process.stdout.write(`\n ${DIM}${skills.length} skills | skills.ws${R}\n\n`); return; } // No args = interactive install if (!args[0]) { const names = await interactivePick(skills); await installSkills(names, skills, customDir); return; } // Unknown argument: print usage and fail loudly instead of a silent no-op process.stderr.write(` ${YELLOW}Unknown command:${R} ${args[0]}\n\n`); process.stderr.write(` ${B}Usage:${R} npx skills-ws [list | install <name...> | install all | help]\n\n`); process.exit(1); } process.on("SIGINT", () => { showCursor(); process.exit(0); }); process.on("exit", () => { showCursor(); }); main().catch((err) => { showCursor(); console.error(err); process.exit(1); });