skillsets 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.
@@ -0,0 +1,452 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { input, confirm, checkbox } from '@inquirer/prompts';
4
+ import { existsSync, mkdirSync, copyFileSync, readdirSync, writeFileSync } from 'fs';
5
+ import { join } from 'path';
6
+ const SKILLSET_YAML_TEMPLATE = `schema_version: "1.0"
7
+
8
+ # Identity
9
+ name: "{{NAME}}"
10
+ version: "1.0.0"
11
+ description: "{{DESCRIPTION}}"
12
+
13
+ author:
14
+ handle: "{{AUTHOR_HANDLE}}"
15
+ url: "{{AUTHOR_URL}}"
16
+
17
+ # Verification
18
+ verification:
19
+ production_url: "{{PRODUCTION_URL}}"
20
+ production_proof: "./PROOF.md"
21
+ audit_report: "./AUDIT_REPORT.md"
22
+
23
+ # Discovery
24
+ tags:
25
+ {{TAGS}}
26
+
27
+ compatibility:
28
+ claude_code_version: ">=1.0.0"
29
+ languages:
30
+ - "any"
31
+
32
+ # Lifecycle
33
+ status: "active"
34
+
35
+ # Content
36
+ entry_point: "./content/CLAUDE.md"
37
+ `;
38
+ const README_TEMPLATE = `# {{NAME}}
39
+
40
+ {{DESCRIPTION}}
41
+
42
+ ## Installation
43
+
44
+ \`\`\`bash
45
+ npx skillsets install {{AUTHOR_HANDLE}}/{{NAME}}
46
+ \`\`\`
47
+
48
+ ## Usage
49
+
50
+ [Describe how to use your skillset]
51
+
52
+ ## What's Included
53
+
54
+ [List the key files and their purposes]
55
+
56
+ ## License
57
+
58
+ [Your license]
59
+ `;
60
+ const PROOF_TEMPLATE = `# Production Proof
61
+
62
+ ## Overview
63
+
64
+ This skillset has been verified in production.
65
+
66
+ ## Production URL
67
+
68
+ {{PRODUCTION_URL}}
69
+
70
+ ## Evidence
71
+
72
+ [Add screenshots, testimonials, or other evidence of production usage]
73
+
74
+ ## Projects Built
75
+
76
+ [List projects or products built using this skillset]
77
+ `;
78
+ const AUDIT_SKILL_MD = `---
79
+ name: skillset-audit
80
+ description: Qualitative review of skillset content against Claude Code best practices. Evaluates all primitives (skills, agents, hooks, MCP, CLAUDE.md) for proper frontmatter, descriptions, and structure. Appends analysis to AUDIT_REPORT.md.
81
+ ---
82
+
83
+ # Skillset Qualitative Audit
84
+
85
+ ## Task
86
+
87
+ 1. Verify \`AUDIT_REPORT.md\` shows "READY FOR SUBMISSION"
88
+ 2. Identify all primitives in \`content/\`:
89
+ - Skills: \`**/SKILL.md\`
90
+ - Agents: \`**/AGENT.md\` or \`**/*.agent.md\`
91
+ - Hooks: \`**/hooks.json\`
92
+ - MCP: \`**/.mcp.json\` or \`**/mcp.json\`
93
+ - CLAUDE.md: \`CLAUDE.md\` or \`.claude/settings.json\`
94
+ 3. Evaluate each against [CRITERIA.md](CRITERIA.md)
95
+ 4. Append findings to \`AUDIT_REPORT.md\`
96
+
97
+ ## Per-Primitive Evaluation
98
+
99
+ ### Skills
100
+ - Frontmatter has \`name\` and \`description\`
101
+ - Description includes trigger phrases ("Use when...")
102
+ - Body under 500 lines
103
+ - \`allowed-tools\` if restricting access
104
+ - \`disable-model-invocation\` for side-effect commands
105
+
106
+ ### Agents
107
+ - Description has \`<example>\` blocks
108
+ - System prompt has role, responsibilities, process, output format
109
+ - \`tools\` array if restricting access
110
+
111
+ ### Hooks
112
+ - Valid JSON structure
113
+ - Matchers are specific (not just \`.*\`)
114
+ - Reasonable timeouts
115
+ - Prompts are actionable
116
+
117
+ ### MCP
118
+ - Uses \`\${CLAUDE_PLUGIN_ROOT}\` for paths
119
+ - Env vars use \`\${VAR}\` syntax
120
+ - No hardcoded secrets
121
+
122
+ ### CLAUDE.md
123
+ - Under 300 lines (check line count)
124
+ - Has WHAT/WHY/HOW sections
125
+ - Uses \`file:line\` pointers, not code snippets
126
+ - Progressive disclosure for large content
127
+
128
+ ## Output
129
+
130
+ Append to \`AUDIT_REPORT.md\`:
131
+
132
+ \`\`\`markdown
133
+ ---
134
+
135
+ ## Qualitative Review
136
+
137
+ **Reviewed by:** Claude (Opus)
138
+ **Date:** [ISO timestamp]
139
+
140
+ ### Primitives Found
141
+
142
+ | Type | Count | Files |
143
+ |------|-------|-------|
144
+ | Skills | N | [list] |
145
+ | Agents | N | [list] |
146
+ | Hooks | N | [list] |
147
+ | MCP | N | [list] |
148
+ | CLAUDE.md | Y/N | [path] |
149
+
150
+ ### Issues
151
+
152
+ [List each issue with file:line and specific fix needed]
153
+
154
+ ### Verdict
155
+
156
+ **[APPROVED / NEEDS REVISION]**
157
+
158
+ [If needs revision: prioritized list of must-fix items]
159
+ \`\`\`
160
+ `;
161
+ const AUDIT_CRITERIA_MD = `# Evaluation Criteria
162
+
163
+ Rubric for qualitative skillset review. Each primitive type has specific requirements.
164
+
165
+ ---
166
+
167
+ ## Skills (SKILL.md)
168
+
169
+ Skills and slash commands are now unified. File at \`.claude/skills/[name]/SKILL.md\` creates \`/name\`.
170
+
171
+ ### Frontmatter Requirements
172
+
173
+ | Field | Required | Notes |
174
+ |-------|----------|-------|
175
+ | \`name\` | Yes | Becomes the \`/slash-command\`, lowercase with hyphens |
176
+ | \`description\` | Yes | **Critical for discoverability** - Claude uses this to decide when to load |
177
+ | \`version\` | No | Semver for tracking |
178
+ | \`allowed-tools\` | No | Restricts tool access (e.g., \`Read, Write, Bash(git:*)\`) |
179
+ | \`model\` | No | \`sonnet\`, \`opus\`, or \`haiku\` |
180
+ | \`disable-model-invocation\` | No | \`true\` = only user can invoke (for side-effect commands) |
181
+ | \`user-invocable\` | No | \`false\` = only Claude can invoke (background knowledge) |
182
+
183
+ ### Description Quality
184
+
185
+ **GOOD:** Includes trigger phrases ("Use when reviewing PRs, checking vulnerabilities...")
186
+ **POOR:** Vague ("Helps with code review")
187
+
188
+ ---
189
+
190
+ ## Agents (AGENT.md)
191
+
192
+ ### Frontmatter Requirements
193
+
194
+ | Field | Required | Notes |
195
+ |-------|----------|-------|
196
+ | \`name\` | Yes | Agent identifier |
197
+ | \`description\` | Yes | **Must include \`<example>\` blocks** for reliable triggering |
198
+ | \`model\` | No | \`inherit\`, \`sonnet\`, \`opus\`, \`haiku\` |
199
+ | \`color\` | No | UI color hint |
200
+ | \`tools\` | No | Array of allowed tools |
201
+
202
+ ### System Prompt (Body)
203
+
204
+ - Clear role definition ("You are...")
205
+ - Core responsibilities numbered
206
+ - Process/workflow steps
207
+ - Expected output format
208
+
209
+ ---
210
+
211
+ ## Hooks (hooks.json)
212
+
213
+ ### Event Types
214
+
215
+ | Event | When | Use For |
216
+ |-------|------|---------|
217
+ | \`PreToolUse\` | Before tool executes | Validation, security checks |
218
+ | \`PostToolUse\` | After tool completes | Feedback, logging |
219
+ | \`Stop\` | Task completion | Quality gates, notifications |
220
+ | \`SessionStart\` | Session begins | Context loading, env setup |
221
+
222
+ ### Quality Checks
223
+
224
+ - Matchers are specific (avoid \`.*\` unless intentional)
225
+ - Timeouts are reasonable
226
+ - Prompts are concise and actionable
227
+
228
+ ---
229
+
230
+ ## MCP Servers (.mcp.json)
231
+
232
+ ### Quality Checks
233
+
234
+ - Uses \`\${CLAUDE_PLUGIN_ROOT}\` for paths
235
+ - Environment variables use \`\${VAR}\` syntax
236
+ - Sensitive values reference env vars, not hardcoded
237
+
238
+ ---
239
+
240
+ ## CLAUDE.md
241
+
242
+ ### Critical Constraints
243
+
244
+ - **Under 300 lines** (ideally <60)
245
+ - LLMs follow ~150-200 instructions; Claude Code system prompt uses ~50
246
+
247
+ ### Required Content (WHAT, WHY, HOW)
248
+
249
+ - **WHAT**: Tech stack, project structure, codebase map
250
+ - **WHY**: Project purpose, component functions
251
+ - **HOW**: Dev workflows, tools, testing, verification
252
+
253
+ ### What to Avoid
254
+
255
+ - Task-specific instructions
256
+ - Code style rules (use linters + hooks)
257
+ - Code snippets (use \`file:line\` pointers)
258
+ - Hardcoded dates/versions
259
+
260
+ ---
261
+
262
+ ## Verdict Rules
263
+
264
+ - **APPROVED**: All primitives meet requirements, minor issues only
265
+ - **NEEDS REVISION**: Missing required fields, poor descriptions, oversized files
266
+
267
+ Priority:
268
+ 1. Missing/poor descriptions (affects discoverability)
269
+ 2. Oversized CLAUDE.md (degrades all instructions)
270
+ 3. Missing agent examples (unreliable triggering)
271
+ `;
272
+ function copyDirRecursive(src, dest) {
273
+ if (!existsSync(dest)) {
274
+ mkdirSync(dest, { recursive: true });
275
+ }
276
+ const entries = readdirSync(src, { withFileTypes: true });
277
+ for (const entry of entries) {
278
+ const srcPath = join(src, entry.name);
279
+ const destPath = join(dest, entry.name);
280
+ if (entry.isDirectory()) {
281
+ copyDirRecursive(srcPath, destPath);
282
+ }
283
+ else {
284
+ copyFileSync(srcPath, destPath);
285
+ }
286
+ }
287
+ }
288
+ export async function init(options) {
289
+ console.log(chalk.blue('\nšŸ“¦ Initialize a new skillset submission\n'));
290
+ const cwd = process.cwd();
291
+ // Check if already initialized
292
+ if (existsSync(join(cwd, 'skillset.yaml'))) {
293
+ console.log(chalk.yellow('⚠ skillset.yaml already exists in this directory.'));
294
+ const overwrite = await confirm({
295
+ message: 'Overwrite existing files?',
296
+ default: false,
297
+ });
298
+ if (!overwrite) {
299
+ console.log(chalk.gray('Aborted.'));
300
+ return;
301
+ }
302
+ }
303
+ // Gather information
304
+ const name = await input({
305
+ message: 'Skillset name (alphanumeric, hyphens, underscores):',
306
+ validate: (value) => {
307
+ if (!/^[A-Za-z0-9_-]+$/.test(value)) {
308
+ return 'Name must be alphanumeric with hyphens/underscores only';
309
+ }
310
+ if (value.length < 1 || value.length > 100) {
311
+ return 'Name must be 1-100 characters';
312
+ }
313
+ return true;
314
+ },
315
+ });
316
+ const description = await input({
317
+ message: 'Description (10-200 characters):',
318
+ validate: (value) => {
319
+ if (value.length < 10 || value.length > 200) {
320
+ return 'Description must be 10-200 characters';
321
+ }
322
+ return true;
323
+ },
324
+ });
325
+ const authorHandle = await input({
326
+ message: 'GitHub handle (e.g., @username):',
327
+ validate: (value) => {
328
+ if (!/^@[A-Za-z0-9_-]+$/.test(value)) {
329
+ return 'Handle must start with @ followed by alphanumeric characters';
330
+ }
331
+ return true;
332
+ },
333
+ });
334
+ const authorUrl = await input({
335
+ message: 'Author URL (GitHub profile or website):',
336
+ default: `https://github.com/${authorHandle.slice(1)}`,
337
+ });
338
+ const productionUrl = await input({
339
+ message: 'Production URL (live deployment, repo, or case study):',
340
+ validate: (value) => {
341
+ try {
342
+ new URL(value);
343
+ return true;
344
+ }
345
+ catch {
346
+ return 'Must be a valid URL';
347
+ }
348
+ },
349
+ });
350
+ const tagsInput = await input({
351
+ message: 'Tags (comma-separated, lowercase, e.g., sdlc,planning,multi-agent):',
352
+ validate: (value) => {
353
+ const tags = value.split(',').map((t) => t.trim());
354
+ if (tags.length < 1 || tags.length > 10) {
355
+ return 'Must have 1-10 tags';
356
+ }
357
+ for (const tag of tags) {
358
+ if (!/^[a-z0-9-]+$/.test(tag)) {
359
+ return `Tag "${tag}" must be lowercase alphanumeric with hyphens only`;
360
+ }
361
+ }
362
+ return true;
363
+ },
364
+ });
365
+ const tags = tagsInput.split(',').map((t) => t.trim());
366
+ // Auto-detect existing files
367
+ const detectedFiles = [];
368
+ if (existsSync(join(cwd, '.claude'))) {
369
+ detectedFiles.push('.claude/');
370
+ }
371
+ if (existsSync(join(cwd, 'CLAUDE.md'))) {
372
+ detectedFiles.push('CLAUDE.md');
373
+ }
374
+ let filesToCopy = [];
375
+ if (detectedFiles.length > 0) {
376
+ console.log(chalk.green('\nāœ“ Detected existing skillset files:'));
377
+ detectedFiles.forEach((f) => console.log(` - ${f}`));
378
+ filesToCopy = await checkbox({
379
+ message: 'Select files to copy to content/:',
380
+ choices: detectedFiles.map((f) => ({ name: f, value: f, checked: true })),
381
+ });
382
+ }
383
+ // Create structure
384
+ const spinner = ora('Creating skillset structure...').start();
385
+ try {
386
+ // Create content directory
387
+ mkdirSync(join(cwd, 'content'), { recursive: true });
388
+ // Copy selected files to content/
389
+ for (const file of filesToCopy) {
390
+ const src = join(cwd, file);
391
+ const dest = join(cwd, 'content', file);
392
+ if (file.endsWith('/')) {
393
+ // Directory
394
+ copyDirRecursive(src.slice(0, -1), dest.slice(0, -1));
395
+ }
396
+ else {
397
+ // File
398
+ copyFileSync(src, dest);
399
+ }
400
+ }
401
+ // Generate skillset.yaml
402
+ const tagsYaml = tags.map((t) => ` - "${t}"`).join('\n');
403
+ const skillsetYaml = SKILLSET_YAML_TEMPLATE
404
+ .replace('{{NAME}}', name)
405
+ .replace('{{DESCRIPTION}}', description)
406
+ .replace('{{AUTHOR_HANDLE}}', authorHandle)
407
+ .replace('{{AUTHOR_URL}}', authorUrl)
408
+ .replace('{{PRODUCTION_URL}}', productionUrl)
409
+ .replace('{{TAGS}}', tagsYaml);
410
+ writeFileSync(join(cwd, 'skillset.yaml'), skillsetYaml);
411
+ // Generate README.md (if not copying existing)
412
+ if (!existsSync(join(cwd, 'README.md'))) {
413
+ const readme = README_TEMPLATE
414
+ .replace(/\{\{NAME\}\}/g, name)
415
+ .replace(/\{\{DESCRIPTION\}\}/g, description)
416
+ .replace(/\{\{AUTHOR_HANDLE\}\}/g, authorHandle);
417
+ writeFileSync(join(cwd, 'README.md'), readme);
418
+ }
419
+ // Generate PROOF.md
420
+ const proof = PROOF_TEMPLATE.replace('{{PRODUCTION_URL}}', productionUrl);
421
+ writeFileSync(join(cwd, 'PROOF.md'), proof);
422
+ // Install skillset-audit skill to .claude/skills/
423
+ const skillDir = join(cwd, '.claude', 'skills', 'skillset-audit');
424
+ mkdirSync(skillDir, { recursive: true });
425
+ writeFileSync(join(skillDir, 'SKILL.md'), AUDIT_SKILL_MD);
426
+ writeFileSync(join(skillDir, 'CRITERIA.md'), AUDIT_CRITERIA_MD);
427
+ spinner.succeed('Skillset structure created');
428
+ // Summary
429
+ console.log(chalk.green('\nāœ“ Initialized skillset submission:\n'));
430
+ console.log(' skillset.yaml - Manifest (edit as needed)');
431
+ console.log(' README.md - Documentation');
432
+ console.log(' PROOF.md - Production evidence (add details)');
433
+ console.log(' content/ - Installable files');
434
+ if (filesToCopy.length > 0) {
435
+ filesToCopy.forEach((f) => console.log(` └── ${f}`));
436
+ }
437
+ else {
438
+ console.log(' └── (add your .claude/ and/or CLAUDE.md here)');
439
+ }
440
+ console.log(' .claude/skills/ - Audit skill installed');
441
+ console.log(' └── skillset-audit/');
442
+ console.log(chalk.cyan('\nNext steps:'));
443
+ console.log(' 1. Edit PROOF.md with production evidence');
444
+ console.log(' 2. Ensure content/ has your skillset files');
445
+ console.log(' 3. Run: npx skillsets audit');
446
+ console.log(' 4. Run: /skillset-audit (qualitative review)');
447
+ }
448
+ catch (error) {
449
+ spinner.fail('Failed to create structure');
450
+ throw error;
451
+ }
452
+ }
@@ -0,0 +1,6 @@
1
+ interface InstallOptions {
2
+ force?: boolean;
3
+ backup?: boolean;
4
+ }
5
+ export declare function install(skillsetId: string, options: InstallOptions): Promise<void>;
6
+ export {};
@@ -0,0 +1,54 @@
1
+ import degit from 'degit';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { detectConflicts, backupFiles } from '../lib/filesystem.js';
5
+ import { verifyChecksums } from '../lib/checksum.js';
6
+ import { REGISTRY_REPO } from '../lib/constants.js';
7
+ export async function install(skillsetId, options) {
8
+ const spinner = ora(`Installing ${skillsetId}...`).start();
9
+ // Check for conflicts
10
+ const conflicts = await detectConflicts(process.cwd());
11
+ if (conflicts.length > 0 && !options.force && !options.backup) {
12
+ spinner.fail('Installation aborted');
13
+ console.log(chalk.yellow('\nExisting files detected:'));
14
+ conflicts.forEach((file) => console.log(` - ${file}`));
15
+ console.log(chalk.cyan('\nUse one of these flags:'));
16
+ console.log(' --force Overwrite existing files');
17
+ console.log(' --backup Backup existing files before install');
18
+ return;
19
+ }
20
+ // Backup if requested
21
+ if (options.backup && conflicts.length > 0) {
22
+ spinner.text = 'Backing up existing files...';
23
+ await backupFiles(conflicts, process.cwd());
24
+ }
25
+ // Install using degit (extract content/ subdirectory)
26
+ spinner.text = 'Downloading skillset...';
27
+ const emitter = degit(`${REGISTRY_REPO}/skillsets/${skillsetId}/content`, {
28
+ cache: true,
29
+ force: true,
30
+ verbose: false,
31
+ });
32
+ await emitter.clone(process.cwd());
33
+ // Verify checksums
34
+ spinner.text = 'Verifying checksums...';
35
+ const result = await verifyChecksums(skillsetId, process.cwd());
36
+ if (!result.valid) {
37
+ spinner.fail('Checksum verification failed - files may be corrupted');
38
+ console.log(chalk.red('\nInstallation aborted due to checksum mismatch.'));
39
+ console.log(chalk.yellow('This could indicate:'));
40
+ console.log(' - Network issues during download');
41
+ console.log(' - Corrupted files in the registry');
42
+ console.log(' - Tampering with the downloaded content');
43
+ console.log(chalk.cyan('\nTo retry:'));
44
+ console.log(` npx skillsets install ${skillsetId} --force`);
45
+ process.exit(1);
46
+ }
47
+ spinner.succeed(`Successfully installed ${skillsetId}`);
48
+ // Print next steps
49
+ console.log(chalk.green('\nāœ“ Installation complete!'));
50
+ console.log(chalk.gray('\nNext steps:'));
51
+ console.log(' 1. Review CLAUDE.md for usage instructions');
52
+ console.log(' 2. Customize .claude/skills/ for your project');
53
+ console.log(' 3. Run: claude');
54
+ }
@@ -0,0 +1,7 @@
1
+ interface ListOptions {
2
+ limit?: string;
3
+ sort?: 'name' | 'stars' | 'recent';
4
+ json?: boolean;
5
+ }
6
+ export declare function list(options: ListOptions): Promise<void>;
7
+ export {};
@@ -0,0 +1,72 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { fetchSearchIndex } from '../lib/api.js';
4
+ export async function list(options) {
5
+ const spinner = ora('Fetching skillsets...').start();
6
+ try {
7
+ const index = await fetchSearchIndex();
8
+ spinner.stop();
9
+ let skillsets = [...index.skillsets];
10
+ // Sort
11
+ const sortBy = options.sort || 'name';
12
+ if (sortBy === 'stars') {
13
+ skillsets.sort((a, b) => b.stars - a.stars);
14
+ }
15
+ else if (sortBy === 'name') {
16
+ skillsets.sort((a, b) => a.name.localeCompare(b.name));
17
+ }
18
+ // 'recent' would require a date field - skip for now
19
+ // Limit
20
+ const limit = parseInt(options.limit || '0', 10);
21
+ if (limit > 0) {
22
+ skillsets = skillsets.slice(0, limit);
23
+ }
24
+ // JSON output
25
+ if (options.json) {
26
+ console.log(JSON.stringify(skillsets, null, 2));
27
+ return;
28
+ }
29
+ // No results
30
+ if (skillsets.length === 0) {
31
+ console.log(chalk.yellow('No skillsets found in the registry.'));
32
+ console.log(chalk.gray('\nBe the first to contribute: npx skillsets init'));
33
+ return;
34
+ }
35
+ // Header
36
+ console.log(chalk.bold(`\nšŸ“¦ Available Skillsets (${skillsets.length})\n`));
37
+ // Table header
38
+ console.log(chalk.gray(padEnd('NAME', 30) +
39
+ padEnd('AUTHOR', 20) +
40
+ padEnd('STARS', 8) +
41
+ 'DESCRIPTION'));
42
+ console.log(chalk.gray('─'.repeat(100)));
43
+ // Rows
44
+ for (const s of skillsets) {
45
+ const name = padEnd(s.name, 30);
46
+ const author = padEnd(s.author.handle, 20);
47
+ const stars = padEnd(`ā˜… ${s.stars}`, 8);
48
+ const desc = truncate(s.description, 40);
49
+ console.log(chalk.bold(name) +
50
+ chalk.gray(author) +
51
+ chalk.yellow(stars) +
52
+ desc);
53
+ }
54
+ console.log('');
55
+ console.log(chalk.gray(`Install: npx skillsets install <name>`));
56
+ console.log(chalk.gray(`Details: npx skillsets search <name>`));
57
+ }
58
+ catch (error) {
59
+ spinner.fail('Failed to fetch skillsets');
60
+ throw error;
61
+ }
62
+ }
63
+ function padEnd(str, len) {
64
+ if (str.length >= len)
65
+ return str.slice(0, len - 1) + ' ';
66
+ return str + ' '.repeat(len - str.length);
67
+ }
68
+ function truncate(str, len) {
69
+ if (str.length <= len)
70
+ return str;
71
+ return str.slice(0, len - 3) + '...';
72
+ }
@@ -0,0 +1,6 @@
1
+ interface SearchOptions {
2
+ tags?: string[];
3
+ limit?: string;
4
+ }
5
+ export declare function search(query: string, options: SearchOptions): Promise<void>;
6
+ export {};
@@ -0,0 +1,38 @@
1
+ import Fuse from 'fuse.js';
2
+ import chalk from 'chalk';
3
+ import { fetchSearchIndex } from '../lib/api.js';
4
+ import { DEFAULT_SEARCH_LIMIT } from '../lib/constants.js';
5
+ export async function search(query, options) {
6
+ console.log(chalk.blue(`Searching for: ${query}`));
7
+ // Fetch index from CDN
8
+ const index = await fetchSearchIndex();
9
+ // Filter by tags if provided
10
+ let filtered = index.skillsets;
11
+ if (options.tags && options.tags.length > 0) {
12
+ filtered = filtered.filter((skillset) => options.tags.some((tag) => skillset.tags.includes(tag)));
13
+ }
14
+ // Fuzzy search
15
+ const fuse = new Fuse(filtered, {
16
+ keys: ['name', 'description', 'tags', 'author.handle'],
17
+ threshold: 0.3,
18
+ includeScore: true,
19
+ });
20
+ const results = fuse.search(query);
21
+ const limit = parseInt(options.limit || DEFAULT_SEARCH_LIMIT.toString(), 10);
22
+ if (results.length === 0) {
23
+ console.log(chalk.yellow('No results found.'));
24
+ return;
25
+ }
26
+ console.log(chalk.green(`\nFound ${results.length} result(s):\n`));
27
+ results.slice(0, limit).forEach(({ item }) => {
28
+ console.log(chalk.bold(item.name));
29
+ console.log(` ${item.description}`);
30
+ console.log(` ${chalk.gray(`by ${item.author.handle}`)}`);
31
+ console.log(` ${chalk.yellow(`ā˜… ${item.stars}`)} ${chalk.gray(`• v${item.version}`)}`);
32
+ console.log(` ${chalk.cyan(`npx skillsets install ${item.id}`)}`);
33
+ console.log();
34
+ });
35
+ if (results.length > limit) {
36
+ console.log(chalk.gray(`... and ${results.length - limit} more`));
37
+ }
38
+ }
@@ -0,0 +1 @@
1
+ export declare function submit(): Promise<void>;