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.
- package/README.md +41 -0
- package/dist/commands/audit.d.ts +1 -0
- package/dist/commands/audit.js +472 -0
- package/dist/commands/init.d.ts +5 -0
- package/dist/commands/init.js +452 -0
- package/dist/commands/install.d.ts +6 -0
- package/dist/commands/install.js +54 -0
- package/dist/commands/list.d.ts +7 -0
- package/dist/commands/list.js +72 -0
- package/dist/commands/search.d.ts +6 -0
- package/dist/commands/search.js +38 -0
- package/dist/commands/submit.d.ts +1 -0
- package/dist/commands/submit.js +292 -0
- package/dist/commands/verify.d.ts +5 -0
- package/dist/commands/verify.js +31 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +105 -0
- package/dist/lib/api.d.ts +10 -0
- package/dist/lib/api.js +29 -0
- package/dist/lib/checksum.d.ts +16 -0
- package/dist/lib/checksum.js +53 -0
- package/dist/lib/constants.d.ts +6 -0
- package/dist/lib/constants.js +6 -0
- package/dist/lib/errors.d.ts +1 -0
- package/dist/lib/errors.js +11 -0
- package/dist/lib/filesystem.d.ts +13 -0
- package/dist/lib/filesystem.js +75 -0
- package/dist/types/index.d.ts +52 -0
- package/dist/types/index.js +1 -0
- package/package.json +54 -0
|
@@ -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,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,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,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>;
|