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.
Files changed (2) hide show
  1. package/bin/vibeindex.mjs +263 -0
  2. 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
+ }