@spardutti/claude-skills 1.9.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.mjs +41 -22
- package/lib/github.mjs +45 -0
- package/lib/install.mjs +10 -0
- package/lib/prompt.mjs +46 -0
- package/lib/setup-hook.mjs +6 -2
- package/package.json +1 -1
package/bin/cli.mjs
CHANGED
|
@@ -5,9 +5,9 @@ import chalk from "chalk";
|
|
|
5
5
|
import { readFileSync } from "node:fs";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { dirname, join } from "node:path";
|
|
8
|
-
import { fetchSkills } from "../lib/github.mjs";
|
|
9
|
-
import { promptSkillSelection } from "../lib/prompt.mjs";
|
|
10
|
-
import { installSkills } from "../lib/install.mjs";
|
|
8
|
+
import { fetchSkills, fetchCommands } from "../lib/github.mjs";
|
|
9
|
+
import { promptSkillSelection, promptCommandSelection } from "../lib/prompt.mjs";
|
|
10
|
+
import { installSkills, installCommands } from "../lib/install.mjs";
|
|
11
11
|
import { setupHook } from "../lib/setup-hook.mjs";
|
|
12
12
|
import { setupClaudeMd } from "../lib/setup-claude-md.mjs";
|
|
13
13
|
|
|
@@ -17,37 +17,56 @@ const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-
|
|
|
17
17
|
async function main() {
|
|
18
18
|
console.log(`\n ${chalk.bold.cyan("Claude Skills Installer")} ${chalk.dim(`v${pkg.version}`)}\n`);
|
|
19
19
|
|
|
20
|
-
console.log(chalk.dim(" Fetching available skills...\n"));
|
|
21
|
-
const skills = await fetchSkills();
|
|
20
|
+
console.log(chalk.dim(" Fetching available skills and commands...\n"));
|
|
21
|
+
const [skills, commands] = await Promise.all([fetchSkills(), fetchCommands()]);
|
|
22
22
|
|
|
23
|
+
// --- Skills ---
|
|
24
|
+
let selectedSkills = [];
|
|
23
25
|
if (skills.length === 0) {
|
|
24
26
|
console.log(" No skills found.");
|
|
25
|
-
|
|
27
|
+
} else {
|
|
28
|
+
selectedSkills = await promptSkillSelection(skills);
|
|
29
|
+
if (selectedSkills.length > 0) {
|
|
30
|
+
console.log();
|
|
31
|
+
await installSkills(selectedSkills);
|
|
32
|
+
}
|
|
26
33
|
}
|
|
27
34
|
|
|
28
|
-
|
|
35
|
+
// --- Commands ---
|
|
36
|
+
let selectedCommands = [];
|
|
37
|
+
if (commands.length > 0) {
|
|
38
|
+
console.log();
|
|
39
|
+
selectedCommands = await promptCommandSelection(commands);
|
|
40
|
+
if (selectedCommands.length > 0) {
|
|
41
|
+
console.log();
|
|
42
|
+
await installCommands(selectedCommands);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
29
45
|
|
|
30
|
-
if (
|
|
31
|
-
console.log("\n
|
|
46
|
+
if (selectedSkills.length === 0 && selectedCommands.length === 0) {
|
|
47
|
+
console.log("\n Nothing selected.");
|
|
32
48
|
process.exit(0);
|
|
33
49
|
}
|
|
34
50
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
console.log();
|
|
39
|
-
const shouldSetup = await confirm({
|
|
40
|
-
message: "Set up skill evaluation hook + CLAUDE.md rule? (Recommended)",
|
|
41
|
-
default: true,
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
if (shouldSetup) {
|
|
51
|
+
// --- Hook setup (only if skills were installed) ---
|
|
52
|
+
if (selectedSkills.length > 0) {
|
|
45
53
|
console.log();
|
|
46
|
-
await
|
|
47
|
-
|
|
54
|
+
const shouldSetup = await confirm({
|
|
55
|
+
message: "Set up skill evaluation hook + CLAUDE.md rule? (Recommended)",
|
|
56
|
+
default: true,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (shouldSetup) {
|
|
60
|
+
console.log();
|
|
61
|
+
await setupHook();
|
|
62
|
+
await setupClaudeMd();
|
|
63
|
+
}
|
|
48
64
|
}
|
|
49
65
|
|
|
50
|
-
|
|
66
|
+
const parts = [];
|
|
67
|
+
if (selectedSkills.length > 0) parts.push(`${selectedSkills.length} skill(s)`);
|
|
68
|
+
if (selectedCommands.length > 0) parts.push(`${selectedCommands.length} command(s)`);
|
|
69
|
+
console.log(`\n ${chalk.green("✔")} ${chalk.bold(`${parts.join(" and ")} installed successfully.`)}\n`);
|
|
51
70
|
}
|
|
52
71
|
|
|
53
72
|
main().catch((err) => {
|
package/lib/github.mjs
CHANGED
|
@@ -3,7 +3,9 @@ import { execSync } from "node:child_process";
|
|
|
3
3
|
const REPO_OWNER = "Spardutti";
|
|
4
4
|
const REPO_NAME = "claude-skills";
|
|
5
5
|
const CONTENTS_API = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/contents/skills`;
|
|
6
|
+
const COMMANDS_API = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/contents/commands`;
|
|
6
7
|
const RAW_BASE = `https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/main/skills`;
|
|
8
|
+
const RAW_COMMANDS_BASE = `https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/main/commands`;
|
|
7
9
|
|
|
8
10
|
function getAuthHeaders() {
|
|
9
11
|
const headers = { "User-Agent": "claude-skills-cli" };
|
|
@@ -72,6 +74,49 @@ export async function fetchSkills() {
|
|
|
72
74
|
return results.filter(Boolean);
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
export async function fetchCommands() {
|
|
78
|
+
const headers = getAuthHeaders();
|
|
79
|
+
|
|
80
|
+
const res = await fetch(COMMANDS_API, { headers });
|
|
81
|
+
|
|
82
|
+
if (!res.ok) {
|
|
83
|
+
if (res.status === 404) {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
if (res.status === 403 || res.status === 429) {
|
|
87
|
+
throw new Error("GitHub API rate limit exceeded. Try again later or install gh CLI (https://cli.github.com).");
|
|
88
|
+
}
|
|
89
|
+
throw new Error(`Failed to list commands: ${res.status} ${res.statusText}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const entries = await res.json();
|
|
93
|
+
const files = entries.filter((e) => e.type === "file" && e.name.endsWith(".md"));
|
|
94
|
+
|
|
95
|
+
const results = await Promise.all(
|
|
96
|
+
files.map(async (file) => {
|
|
97
|
+
try {
|
|
98
|
+
const url = `${RAW_COMMANDS_BASE}/${file.name}`;
|
|
99
|
+
const r = await fetch(url, { headers });
|
|
100
|
+
|
|
101
|
+
if (!r.ok) {
|
|
102
|
+
console.warn(` Warning: Failed to fetch command ${file.name}, skipping`);
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const content = await r.text();
|
|
107
|
+
const { name, description, category } = parseFrontmatter(content, file.name.replace(/\.md$/, ""));
|
|
108
|
+
|
|
109
|
+
return { fileName: file.name, name, description, category, content };
|
|
110
|
+
} catch {
|
|
111
|
+
console.warn(` Warning: Failed to fetch ${file.name}, skipping`);
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
return results.filter(Boolean);
|
|
118
|
+
}
|
|
119
|
+
|
|
75
120
|
function parseFrontmatter(content, fallbackName) {
|
|
76
121
|
const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
77
122
|
if (!match) {
|
package/lib/install.mjs
CHANGED
|
@@ -18,3 +18,13 @@ export async function installSkills(skills, targetDir = process.cwd()) {
|
|
|
18
18
|
console.log(` ${chalk.green("✔")} ${chalk.bold(humanName(skill))} ${chalk.dim(`→ .claude/skills/${skill.dirName}/`)}`);
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
+
|
|
22
|
+
export async function installCommands(commands, targetDir = process.cwd()) {
|
|
23
|
+
const baseDir = join(targetDir, ".claude", "commands");
|
|
24
|
+
await mkdir(baseDir, { recursive: true });
|
|
25
|
+
|
|
26
|
+
for (const cmd of commands) {
|
|
27
|
+
await writeFile(join(baseDir, cmd.fileName), cmd.content);
|
|
28
|
+
console.log(` ${chalk.green("✔")} ${chalk.bold(humanName(cmd))} ${chalk.dim(`→ .claude/commands/${cmd.fileName}`)}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
package/lib/prompt.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { checkbox, Separator } from "@inquirer/prompts";
|
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
|
|
4
4
|
const CATEGORY_ORDER = ["Frontend", "TypeScript", "Backend", "Quality", "General"];
|
|
5
|
+
const COMMAND_CATEGORY_ORDER = ["Workflow", "General"];
|
|
5
6
|
|
|
6
7
|
function humanName(skill) {
|
|
7
8
|
return skill.name
|
|
@@ -51,3 +52,48 @@ export async function promptSkillSelection(skills) {
|
|
|
51
52
|
|
|
52
53
|
return selected;
|
|
53
54
|
}
|
|
55
|
+
|
|
56
|
+
export async function promptCommandSelection(commands) {
|
|
57
|
+
const grouped = new Map();
|
|
58
|
+
for (const cat of COMMAND_CATEGORY_ORDER) {
|
|
59
|
+
const items = commands.filter((c) => c.category === cat);
|
|
60
|
+
if (items.length > 0) grouped.set(cat, items);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Include any categories not in the order list
|
|
64
|
+
for (const cmd of commands) {
|
|
65
|
+
if (!COMMAND_CATEGORY_ORDER.includes(cmd.category)) {
|
|
66
|
+
if (!grouped.has(cmd.category)) grouped.set(cmd.category, []);
|
|
67
|
+
grouped.get(cmd.category).push(cmd);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const choices = [];
|
|
72
|
+
for (const [category, items] of grouped) {
|
|
73
|
+
choices.push(new Separator(`── ${category} ${"─".repeat(35 - category.length)}`));
|
|
74
|
+
for (const cmd of items) {
|
|
75
|
+
choices.push({
|
|
76
|
+
name: chalk.bold(humanName(cmd)),
|
|
77
|
+
value: cmd,
|
|
78
|
+
description: chalk.dim(stripQuotes(cmd.description)),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const theme = {
|
|
84
|
+
icon: { cursor: ">" },
|
|
85
|
+
style: {
|
|
86
|
+
highlight: (text) => chalk.cyan(text),
|
|
87
|
+
renderSelectedChoices: (selected) =>
|
|
88
|
+
selected.map((s) => chalk.cyan(humanName(s))).join(", "),
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const selected = await checkbox({
|
|
93
|
+
message: "Select commands to install:",
|
|
94
|
+
choices,
|
|
95
|
+
theme,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return selected;
|
|
99
|
+
}
|
package/lib/setup-hook.mjs
CHANGED
|
@@ -6,7 +6,10 @@ const HOOK_SCRIPT = `#!/bin/bash
|
|
|
6
6
|
|
|
7
7
|
cat > /dev/null
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
# Derive project root from the hook's own location
|
|
10
|
+
# .claude/hooks/script.sh → go up two levels → project root
|
|
11
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
12
|
+
DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
|
|
10
13
|
|
|
11
14
|
# Build skill list from project skills
|
|
12
15
|
SKILL_LIST=""
|
|
@@ -67,8 +70,9 @@ export async function setupHook(targetDir = process.cwd()) {
|
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
// --- UserPromptSubmit hook (forced eval via command) ---
|
|
73
|
+
const relativeHookPath = `.claude/hooks/${HOOK_FILENAME}`;
|
|
70
74
|
const promptHookEntry = {
|
|
71
|
-
hooks: [{ type: "command", command:
|
|
75
|
+
hooks: [{ type: "command", command: relativeHookPath }],
|
|
72
76
|
};
|
|
73
77
|
|
|
74
78
|
if (Array.isArray(settings.hooks.UserPromptSubmit)) {
|