vibeindex 0.1.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/vibeindex.mjs +263 -0
- package/package.json +26 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// vibeindex CLI - Install Claude Code skills to .claude/skills/
|
|
4
|
+
// Zero dependencies, Node 18+ only
|
|
5
|
+
|
|
6
|
+
import { writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
7
|
+
import { join, dirname } from 'node:path';
|
|
8
|
+
|
|
9
|
+
const VERSION = '0.1.0';
|
|
10
|
+
|
|
11
|
+
// ── Helpers ──────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
function printUsage() {
|
|
14
|
+
console.log(`
|
|
15
|
+
vibeindex v${VERSION} - Install Claude Code skills
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
npx vibeindex add <owner/repo> [--skill <name>]
|
|
19
|
+
npx vibeindex add <github-url> [--skill <name>]
|
|
20
|
+
|
|
21
|
+
Examples:
|
|
22
|
+
npx vibeindex add anthropics/claude-code --skill memory
|
|
23
|
+
npx vibeindex add vercel-labs/skills --skill find-skills
|
|
24
|
+
npx vibeindex add https://github.com/anthropics/claude-code --skill memory
|
|
25
|
+
npx vibeindex add user/repo # single-skill repo (root SKILL.md)
|
|
26
|
+
|
|
27
|
+
Options:
|
|
28
|
+
--skill <name> Skill name (required for multi-skill repos)
|
|
29
|
+
--help Show this help
|
|
30
|
+
--version Show version
|
|
31
|
+
`.trim());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function fatal(msg) {
|
|
35
|
+
console.error(`\x1b[31merror:\x1b[0m ${msg}`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function info(msg) {
|
|
40
|
+
console.log(`\x1b[36m>\x1b[0m ${msg}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function success(msg) {
|
|
44
|
+
console.log(`\x1b[32m✓\x1b[0m ${msg}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── Parse owner/repo from input ──────────────────────────────
|
|
48
|
+
|
|
49
|
+
function parseTarget(input) {
|
|
50
|
+
// GitHub URL: https://github.com/owner/repo[/...]
|
|
51
|
+
const urlMatch = input.match(/github\.com\/([^/]+)\/([^/\s#?]+)/);
|
|
52
|
+
if (urlMatch) {
|
|
53
|
+
return { owner: urlMatch[1], repo: urlMatch[2].replace(/\.git$/, '') };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// owner/repo format
|
|
57
|
+
const parts = input.split('/');
|
|
58
|
+
if (parts.length === 2 && parts[0] && parts[1]) {
|
|
59
|
+
return { owner: parts[0], repo: parts[1] };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── GitHub API (unauthenticated, public repos only) ──────────
|
|
66
|
+
|
|
67
|
+
async function githubGet(url) {
|
|
68
|
+
const res = await fetch(url, {
|
|
69
|
+
headers: { 'User-Agent': 'vibeindex-cli', Accept: 'application/vnd.github.v3+json' },
|
|
70
|
+
});
|
|
71
|
+
if (!res.ok) {
|
|
72
|
+
if (res.status === 403 || res.status === 429) {
|
|
73
|
+
fatal('GitHub API rate limit hit. Try again in a minute or set GITHUB_TOKEN env var.');
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return res.json();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function fetchRaw(owner, repo, branch, path) {
|
|
81
|
+
const url = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}`;
|
|
82
|
+
const res = await fetch(url, { headers: { 'User-Agent': 'vibeindex-cli' } });
|
|
83
|
+
if (!res.ok) return null;
|
|
84
|
+
return res.text();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Find the skill directory ─────────────────────────────────
|
|
88
|
+
|
|
89
|
+
async function findSkillDir(owner, repo, skillName) {
|
|
90
|
+
// Candidate paths, ordered by priority
|
|
91
|
+
const candidates = skillName
|
|
92
|
+
? [
|
|
93
|
+
`skills/${skillName}`,
|
|
94
|
+
`skills/.curated/${skillName}`,
|
|
95
|
+
`.claude/skills/${skillName}`,
|
|
96
|
+
skillName,
|
|
97
|
+
]
|
|
98
|
+
: [];
|
|
99
|
+
|
|
100
|
+
// For each candidate, check if SKILL.md exists (try main, then master)
|
|
101
|
+
for (const branch of ['main', 'master']) {
|
|
102
|
+
for (const dir of candidates) {
|
|
103
|
+
const content = await fetchRaw(owner, repo, branch, `${dir}/SKILL.md`);
|
|
104
|
+
if (content) return { branch, dir, hasSkillMd: true };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Root SKILL.md (single-skill repo)
|
|
108
|
+
const rootContent = await fetchRaw(owner, repo, branch, 'SKILL.md');
|
|
109
|
+
if (rootContent) return { branch, dir: '', hasSkillMd: true };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── Recursively list files via GitHub Contents API ───────────
|
|
116
|
+
|
|
117
|
+
async function listFiles(owner, repo, branch, dirPath) {
|
|
118
|
+
const path = dirPath || '';
|
|
119
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${branch}`;
|
|
120
|
+
const token = process.env.GITHUB_TOKEN;
|
|
121
|
+
|
|
122
|
+
const headers = { 'User-Agent': 'vibeindex-cli', Accept: 'application/vnd.github.v3+json' };
|
|
123
|
+
if (token) headers.Authorization = `token ${token}`;
|
|
124
|
+
|
|
125
|
+
const res = await fetch(url, { headers });
|
|
126
|
+
if (!res.ok) return [];
|
|
127
|
+
|
|
128
|
+
const items = await res.json();
|
|
129
|
+
if (!Array.isArray(items)) return [];
|
|
130
|
+
|
|
131
|
+
const files = [];
|
|
132
|
+
for (const item of items) {
|
|
133
|
+
if (item.type === 'file') {
|
|
134
|
+
files.push({ path: item.path, downloadUrl: item.download_url });
|
|
135
|
+
} else if (item.type === 'dir') {
|
|
136
|
+
const subFiles = await listFiles(owner, repo, branch, item.path);
|
|
137
|
+
files.push(...subFiles);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return files;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── Download and save files ──────────────────────────────────
|
|
144
|
+
|
|
145
|
+
async function downloadFile(url) {
|
|
146
|
+
const res = await fetch(url, { headers: { 'User-Agent': 'vibeindex-cli' } });
|
|
147
|
+
if (!res.ok) return null;
|
|
148
|
+
return res.text();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Main: add command ────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
async function cmdAdd(args) {
|
|
154
|
+
// Parse arguments
|
|
155
|
+
let target = null;
|
|
156
|
+
let skillName = null;
|
|
157
|
+
|
|
158
|
+
for (let i = 0; i < args.length; i++) {
|
|
159
|
+
if (args[i] === '--skill' && i + 1 < args.length) {
|
|
160
|
+
skillName = args[++i];
|
|
161
|
+
} else if (!args[i].startsWith('-')) {
|
|
162
|
+
target = args[i];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!target) {
|
|
167
|
+
fatal('Missing target. Usage: npx vibeindex add <owner/repo> [--skill <name>]');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const parsed = parseTarget(target);
|
|
171
|
+
if (!parsed) {
|
|
172
|
+
fatal(`Invalid target "${target}". Use owner/repo or a GitHub URL.`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const { owner, repo } = parsed;
|
|
176
|
+
const displayName = skillName || repo;
|
|
177
|
+
|
|
178
|
+
info(`Looking for skill in ${owner}/${repo}${skillName ? ` (skill: ${skillName})` : ''}...`);
|
|
179
|
+
|
|
180
|
+
// Find the skill directory
|
|
181
|
+
const found = await findSkillDir(owner, repo, skillName);
|
|
182
|
+
if (!found) {
|
|
183
|
+
const tried = skillName
|
|
184
|
+
? `skills/${skillName}/SKILL.md, ${skillName}/SKILL.md, SKILL.md`
|
|
185
|
+
: 'SKILL.md';
|
|
186
|
+
fatal(`Could not find SKILL.md in ${owner}/${repo}. Tried: ${tried}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const { branch, dir } = found;
|
|
190
|
+
info(`Found skill at ${dir || '(root)'}/ on branch ${branch}`);
|
|
191
|
+
|
|
192
|
+
// List all files in the skill directory
|
|
193
|
+
const dirToList = dir || '';
|
|
194
|
+
const files = await listFiles(owner, repo, branch, dirToList);
|
|
195
|
+
|
|
196
|
+
if (files.length === 0) {
|
|
197
|
+
fatal('No files found in the skill directory.');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Determine the local install path
|
|
201
|
+
const localSkillName = skillName || repo;
|
|
202
|
+
const installDir = join(process.cwd(), '.claude', 'skills', localSkillName);
|
|
203
|
+
|
|
204
|
+
info(`Installing ${files.length} file(s) to .claude/skills/${localSkillName}/`);
|
|
205
|
+
|
|
206
|
+
// Download and save each file
|
|
207
|
+
let saved = 0;
|
|
208
|
+
for (const file of files) {
|
|
209
|
+
const content = await downloadFile(file.downloadUrl);
|
|
210
|
+
if (content === null) {
|
|
211
|
+
console.log(` \x1b[33mskip:\x1b[0m ${file.path} (download failed)`);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Compute relative path within the skill dir
|
|
216
|
+
const relativePath = dir ? file.path.slice(dir.length + 1) : file.path;
|
|
217
|
+
const localPath = join(installDir, relativePath);
|
|
218
|
+
|
|
219
|
+
// Ensure parent directory exists
|
|
220
|
+
const parentDir = dirname(localPath);
|
|
221
|
+
if (!existsSync(parentDir)) {
|
|
222
|
+
mkdirSync(parentDir, { recursive: true });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
writeFileSync(localPath, content, 'utf-8');
|
|
226
|
+
saved++;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
console.log('');
|
|
230
|
+
success(`Installed ${saved} file(s) to .claude/skills/${localSkillName}/`);
|
|
231
|
+
console.log('');
|
|
232
|
+
console.log(' Claude Code will automatically pick up this skill.');
|
|
233
|
+
console.log(` To verify: cat .claude/skills/${localSkillName}/SKILL.md`);
|
|
234
|
+
console.log('');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── CLI entry point ──────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
const args = process.argv.slice(2);
|
|
240
|
+
|
|
241
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
242
|
+
console.log(VERSION);
|
|
243
|
+
process.exit(0);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (args.includes('--help') || args.includes('-h') || args.length === 0) {
|
|
247
|
+
printUsage();
|
|
248
|
+
process.exit(0);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const command = args[0];
|
|
252
|
+
const commandArgs = args.slice(1);
|
|
253
|
+
|
|
254
|
+
switch (command) {
|
|
255
|
+
case 'add':
|
|
256
|
+
case 'install':
|
|
257
|
+
cmdAdd(commandArgs).catch(err => {
|
|
258
|
+
fatal(err.message);
|
|
259
|
+
});
|
|
260
|
+
break;
|
|
261
|
+
default:
|
|
262
|
+
fatal(`Unknown command "${command}". Run "npx vibeindex --help" for usage.`);
|
|
263
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vibeindex",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Install Claude Code skills to .claude/skills/",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"vibeindex": "bin/vibeindex.mjs"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"claude",
|
|
14
|
+
"claude-code",
|
|
15
|
+
"skills",
|
|
16
|
+
"ai",
|
|
17
|
+
"mcp"
|
|
18
|
+
],
|
|
19
|
+
"author": "Vibe Index",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"homepage": "https://vibeindex.ai",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/taehojo/vibeindex.git"
|
|
25
|
+
}
|
|
26
|
+
}
|