Files
Quanlai Li b9ebf27f1f Document allowed-tools field, fix CLI double-clone, add descriptions to list
- Document allowed-tools YAML frontmatter field in CONTRIBUTING.md
- Fix install --all cloning repo twice (now clones once and reuses)
- Show skill descriptions from YAML frontmatter in list command
- Delete merged skill/linkedin-ads branch

Closes #3, #4, #5, #6

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 17:14:01 -08:00

179 lines
5.3 KiB
JavaScript
Executable File

#!/usr/bin/env node
import { execSync } from "node:child_process";
import { existsSync, mkdirSync, cpSync, rmSync, readFileSync } from "node:fs";
import { readdirSync } from "node:fs";
import { join } from "node:path";
import { homedir, tmpdir } from "node:os";
const REPO = "https://github.com/OpenClaudia/openclaudia-skills.git";
const SKILLS_DIR = join(homedir(), ".claude", "skills");
const args = process.argv.slice(2);
const command = args[0];
function printHelp() {
console.log(`
openclaudia - Marketing skills for Claude Code
Usage:
npx openclaudia install --all Install all skills
npx openclaudia install <skill-name> Install a specific skill
npx openclaudia list List available skills
npx openclaudia remove <skill-name> Remove a skill
npx openclaudia remove --all Remove all OpenClaudia skills
Examples:
npx openclaudia install --all
npx openclaudia install seo-audit
npx openclaudia install write-blog keyword-research
npx openclaudia list
Website: https://openclaudia.com
GitHub: https://github.com/OpenClaudia/openclaudia-skills
`);
}
function cloneRepo() {
const tmpDir = join(tmpdir(), `openclaudia-${Date.now()}`);
console.log("Fetching skills from GitHub...");
execSync(`git clone --depth 1 ${REPO} ${tmpDir}`, { stdio: "pipe" });
return tmpDir;
}
function getAvailableSkills(repoDir) {
const skillsDir = join(repoDir, "skills");
return readdirSync(skillsDir).filter((f) => {
return existsSync(join(skillsDir, f, "SKILL.md"));
});
}
function getSkillDescription(repoDir, skillName) {
const skillFile = join(repoDir, "skills", skillName, "SKILL.md");
try {
const content = readFileSync(skillFile, "utf-8");
const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
if (match) {
const descMatch = match[1].match(/^description:\s*(.+)$/m);
if (descMatch) return descMatch[1].trim().replace(/^["']|["']$/g, "");
}
} catch {}
return "";
}
function installSkillsFromDir(repoDir, skillNames) {
const repoSkillsDir = join(repoDir, "skills");
const available = getAvailableSkills(repoDir);
if (!existsSync(SKILLS_DIR)) {
mkdirSync(SKILLS_DIR, { recursive: true });
}
let installed = 0;
for (const name of skillNames) {
if (!available.includes(name)) {
console.log(` ! Skill "${name}" not found. Skipping.`);
continue;
}
const src = join(repoSkillsDir, name);
const dest = join(SKILLS_DIR, name);
if (existsSync(dest)) {
console.log(` ~ ${name} (updated)`);
} else {
console.log(` + ${name}`);
}
cpSync(src, dest, { recursive: true });
installed++;
}
return installed;
}
if (!command || command === "help" || command === "--help" || command === "-h") {
printHelp();
process.exit(0);
}
if (command === "list") {
const tmpDir = cloneRepo();
const skills = getAvailableSkills(tmpDir);
console.log(`\n ${skills.length} available skills:\n`);
for (const s of skills.sort()) {
const installed = existsSync(join(SKILLS_DIR, s));
const desc = getSkillDescription(tmpDir, s);
const status = installed ? "[installed]" : " ";
const descStr = desc ? ` ${desc}` : "";
console.log(` ${status} ${s.padEnd(24)}${descStr}`);
}
console.log();
rmSync(tmpDir, { recursive: true, force: true });
process.exit(0);
}
if (command === "install") {
const targets = args.slice(1);
if (targets.length === 0) {
console.log("Usage: npx openclaudia install --all | <skill-name> ...");
process.exit(1);
}
const tmpDir = cloneRepo();
if (targets.includes("--all")) {
const all = getAvailableSkills(tmpDir);
console.log(`\nInstalling all ${all.length} skills to ${SKILLS_DIR}...\n`);
const count = installSkillsFromDir(tmpDir, all);
console.log(`\nDone! ${count} skills installed.`);
console.log(`Skills directory: ${SKILLS_DIR}`);
console.log(`\nRun "claude" and try /write-blog or /seo-audit to get started.\n`);
} else {
console.log(`\nInstalling ${targets.length} skill(s) to ${SKILLS_DIR}...\n`);
const count = installSkillsFromDir(tmpDir, targets);
console.log(`\nDone! ${count} skill(s) installed.\n`);
}
rmSync(tmpDir, { recursive: true, force: true });
process.exit(0);
}
if (command === "remove") {
const targets = args.slice(1);
if (targets.length === 0) {
console.log("Usage: npx openclaudia remove --all | <skill-name> ...");
process.exit(1);
}
if (targets.includes("--all")) {
const tmpDir = cloneRepo();
const all = getAvailableSkills(tmpDir);
rmSync(tmpDir, { recursive: true, force: true });
let removed = 0;
for (const name of all) {
const dest = join(SKILLS_DIR, name);
if (existsSync(dest)) {
rmSync(dest, { recursive: true, force: true });
console.log(` - ${name}`);
removed++;
}
}
console.log(`\nRemoved ${removed} skill(s).\n`);
} else {
for (const name of targets) {
const dest = join(SKILLS_DIR, name);
if (existsSync(dest)) {
rmSync(dest, { recursive: true, force: true });
console.log(` - ${name}`);
} else {
console.log(` ! ${name} not installed. Skipping.`);
}
}
}
process.exit(0);
}
console.log(`Unknown command: ${command}`);
printHelp();
process.exit(1);