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,292 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { existsSync, readFileSync, mkdirSync, cpSync, rmSync } from 'fs';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import yaml from 'js-yaml';
|
|
7
|
+
import { tmpdir } from 'os';
|
|
8
|
+
import { fetchSkillsetMetadata } from '../lib/api.js';
|
|
9
|
+
const REGISTRY_REPO = 'skillsets-cc/main';
|
|
10
|
+
/**
|
|
11
|
+
* Compare semver versions. Returns:
|
|
12
|
+
* -1 if a < b, 0 if a == b, 1 if a > b
|
|
13
|
+
*/
|
|
14
|
+
function compareVersions(a, b) {
|
|
15
|
+
const partsA = a.split('.').map(Number);
|
|
16
|
+
const partsB = b.split('.').map(Number);
|
|
17
|
+
for (let i = 0; i < 3; i++) {
|
|
18
|
+
const numA = partsA[i] || 0;
|
|
19
|
+
const numB = partsB[i] || 0;
|
|
20
|
+
if (numA < numB)
|
|
21
|
+
return -1;
|
|
22
|
+
if (numA > numB)
|
|
23
|
+
return 1;
|
|
24
|
+
}
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
const REGISTRY_URL = `https://github.com/${REGISTRY_REPO}`;
|
|
28
|
+
function checkGhCli() {
|
|
29
|
+
try {
|
|
30
|
+
execSync('gh --version', { stdio: 'ignore' });
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function checkGhAuth() {
|
|
38
|
+
try {
|
|
39
|
+
execSync('gh auth status', { stdio: 'ignore' });
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function getGhUsername() {
|
|
47
|
+
try {
|
|
48
|
+
const result = execSync('gh api user --jq .login', { encoding: 'utf-8' });
|
|
49
|
+
return result.trim();
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function parseSkillsetYaml(cwd) {
|
|
56
|
+
const yamlPath = join(cwd, 'skillset.yaml');
|
|
57
|
+
if (!existsSync(yamlPath))
|
|
58
|
+
return null;
|
|
59
|
+
try {
|
|
60
|
+
const content = readFileSync(yamlPath, 'utf-8');
|
|
61
|
+
const data = yaml.load(content);
|
|
62
|
+
return {
|
|
63
|
+
name: data.name,
|
|
64
|
+
author: data.author?.handle?.replace('@', ''),
|
|
65
|
+
version: data.version,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function checkAuditReport(cwd) {
|
|
73
|
+
const reportPath = join(cwd, 'AUDIT_REPORT.md');
|
|
74
|
+
if (!existsSync(reportPath)) {
|
|
75
|
+
return { exists: false, passing: false };
|
|
76
|
+
}
|
|
77
|
+
const content = readFileSync(reportPath, 'utf-8');
|
|
78
|
+
const passing = content.includes('READY FOR SUBMISSION');
|
|
79
|
+
return { exists: true, passing };
|
|
80
|
+
}
|
|
81
|
+
export async function submit() {
|
|
82
|
+
const cwd = process.cwd();
|
|
83
|
+
console.log(chalk.blue('\nš¤ Submit skillset to registry\n'));
|
|
84
|
+
// Pre-flight checks
|
|
85
|
+
console.log(chalk.gray('Running pre-flight checks...\n'));
|
|
86
|
+
// 1. Check gh CLI
|
|
87
|
+
if (!checkGhCli()) {
|
|
88
|
+
console.log(chalk.red('ā GitHub CLI (gh) not found'));
|
|
89
|
+
console.log(chalk.gray(' Install: https://cli.github.com/'));
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
console.log(chalk.green('ā GitHub CLI found'));
|
|
93
|
+
// 2. Check gh auth
|
|
94
|
+
if (!checkGhAuth()) {
|
|
95
|
+
console.log(chalk.red('ā GitHub CLI not authenticated'));
|
|
96
|
+
console.log(chalk.gray(' Run: gh auth login'));
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
console.log(chalk.green('ā GitHub CLI authenticated'));
|
|
100
|
+
// 3. Get username
|
|
101
|
+
const username = getGhUsername();
|
|
102
|
+
if (!username) {
|
|
103
|
+
console.log(chalk.red('ā Could not determine GitHub username'));
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
console.log(chalk.green(`ā Logged in as ${username}`));
|
|
107
|
+
// 4. Check skillset.yaml
|
|
108
|
+
const skillset = parseSkillsetYaml(cwd);
|
|
109
|
+
if (!skillset) {
|
|
110
|
+
console.log(chalk.red('ā skillset.yaml not found or invalid'));
|
|
111
|
+
console.log(chalk.gray(' Run: npx skillsets init'));
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
console.log(chalk.green(`ā Skillset: ${skillset.name} v${skillset.version}`));
|
|
115
|
+
// 5. Check audit report
|
|
116
|
+
const audit = checkAuditReport(cwd);
|
|
117
|
+
if (!audit.exists) {
|
|
118
|
+
console.log(chalk.red('ā AUDIT_REPORT.md not found'));
|
|
119
|
+
console.log(chalk.gray(' Run: npx skillsets audit'));
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
if (!audit.passing) {
|
|
123
|
+
console.log(chalk.red('ā Audit report shows failures'));
|
|
124
|
+
console.log(chalk.gray(' Fix issues and re-run: npx skillsets audit'));
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
console.log(chalk.green('ā Audit report passing'));
|
|
128
|
+
// 6. Check required files
|
|
129
|
+
const requiredFiles = ['skillset.yaml', 'README.md', 'PROOF.md', 'AUDIT_REPORT.md', 'content'];
|
|
130
|
+
for (const file of requiredFiles) {
|
|
131
|
+
if (!existsSync(join(cwd, file))) {
|
|
132
|
+
console.log(chalk.red(`ā Missing required: ${file}`));
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
console.log(chalk.green('ā All required files present'));
|
|
137
|
+
// 7. Check if this is an update
|
|
138
|
+
const skillsetId = `@${skillset.author}/${skillset.name}`;
|
|
139
|
+
let isUpdate = false;
|
|
140
|
+
let existingVersion = null;
|
|
141
|
+
let registryAvailable = true;
|
|
142
|
+
try {
|
|
143
|
+
const existing = await fetchSkillsetMetadata(skillsetId);
|
|
144
|
+
if (existing) {
|
|
145
|
+
isUpdate = true;
|
|
146
|
+
existingVersion = existing.version;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// Registry unavailable, assume new submission
|
|
151
|
+
registryAvailable = false;
|
|
152
|
+
}
|
|
153
|
+
// Validate version bump (outside try-catch so process.exit works in tests)
|
|
154
|
+
if (isUpdate && existingVersion) {
|
|
155
|
+
if (compareVersions(skillset.version, existingVersion) <= 0) {
|
|
156
|
+
console.log(chalk.red(`ā Version must be greater than ${existingVersion}`));
|
|
157
|
+
console.log(chalk.gray(` Current: ${skillset.version}`));
|
|
158
|
+
console.log(chalk.gray(' Update skillset.yaml with a higher version'));
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
console.log(chalk.green(`ā Update: ${existingVersion} ā ${skillset.version}`));
|
|
162
|
+
}
|
|
163
|
+
else if (registryAvailable) {
|
|
164
|
+
console.log(chalk.green('ā New skillset submission'));
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
console.log(chalk.yellow('ā Could not check registry (assuming new submission)'));
|
|
168
|
+
}
|
|
169
|
+
console.log('');
|
|
170
|
+
// Create submission
|
|
171
|
+
const spinner = ora('Preparing submission...').start();
|
|
172
|
+
const branchName = `submit/${skillset.author}/${skillset.name}`;
|
|
173
|
+
const tempDir = join(tmpdir(), `skillsets-submit-${Date.now()}`);
|
|
174
|
+
try {
|
|
175
|
+
// Fork and clone
|
|
176
|
+
spinner.text = 'Forking registry (if needed)...';
|
|
177
|
+
try {
|
|
178
|
+
execSync(`gh repo fork ${REGISTRY_REPO} --clone=false`, { stdio: 'ignore' });
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
// Already forked, that's fine
|
|
182
|
+
}
|
|
183
|
+
// Clone the fork
|
|
184
|
+
spinner.text = 'Cloning registry...';
|
|
185
|
+
execSync(`gh repo clone ${REGISTRY_REPO} "${tempDir}" -- --depth=1`, { stdio: 'ignore' });
|
|
186
|
+
// Create branch
|
|
187
|
+
spinner.text = 'Creating branch...';
|
|
188
|
+
execSync(`git checkout -b "${branchName}"`, { cwd: tempDir, stdio: 'ignore' });
|
|
189
|
+
// Create skillset directory
|
|
190
|
+
const skillsetDir = join(tempDir, 'skillsets', `@${skillset.author}`, skillset.name);
|
|
191
|
+
mkdirSync(skillsetDir, { recursive: true });
|
|
192
|
+
// Copy files
|
|
193
|
+
spinner.text = 'Copying skillset files...';
|
|
194
|
+
const filesToCopy = ['skillset.yaml', 'README.md', 'PROOF.md', 'AUDIT_REPORT.md', 'content'];
|
|
195
|
+
for (const file of filesToCopy) {
|
|
196
|
+
const src = join(cwd, file);
|
|
197
|
+
const dest = join(skillsetDir, file);
|
|
198
|
+
cpSync(src, dest, { recursive: true });
|
|
199
|
+
}
|
|
200
|
+
// Commit
|
|
201
|
+
spinner.text = 'Committing changes...';
|
|
202
|
+
execSync('git add .', { cwd: tempDir, stdio: 'ignore' });
|
|
203
|
+
const commitMsg = isUpdate
|
|
204
|
+
? `Update ${skillsetId} to v${skillset.version}`
|
|
205
|
+
: `Add ${skillsetId}`;
|
|
206
|
+
execSync(`git commit -m "${commitMsg}"`, { cwd: tempDir, stdio: 'ignore' });
|
|
207
|
+
// Push to fork
|
|
208
|
+
spinner.text = 'Pushing to fork...';
|
|
209
|
+
execSync(`git push -u origin "${branchName}" --force`, { cwd: tempDir, stdio: 'ignore' });
|
|
210
|
+
// Create PR
|
|
211
|
+
spinner.text = 'Creating pull request...';
|
|
212
|
+
const prTitle = isUpdate
|
|
213
|
+
? `Update ${skillsetId} to v${skillset.version}`
|
|
214
|
+
: `Add ${skillsetId}`;
|
|
215
|
+
const prBody = isUpdate
|
|
216
|
+
? `## Skillset Update
|
|
217
|
+
|
|
218
|
+
**Skillset:** ${skillsetId}
|
|
219
|
+
**Version:** ${existingVersion} ā ${skillset.version}
|
|
220
|
+
**Author:** @${skillset.author}
|
|
221
|
+
|
|
222
|
+
### Checklist
|
|
223
|
+
|
|
224
|
+
- [x] \`skillset.yaml\` validated against schema
|
|
225
|
+
- [x] Version bumped from ${existingVersion}
|
|
226
|
+
- [x] \`AUDIT_REPORT.md\` generated and passing
|
|
227
|
+
- [x] \`content/\` directory updated
|
|
228
|
+
|
|
229
|
+
### Changes
|
|
230
|
+
|
|
231
|
+
_Describe what changed in this version._
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
Submitted via \`npx skillsets submit\`
|
|
235
|
+
`
|
|
236
|
+
: `## New Skillset Submission
|
|
237
|
+
|
|
238
|
+
**Skillset:** ${skillsetId}
|
|
239
|
+
**Version:** ${skillset.version}
|
|
240
|
+
**Author:** @${skillset.author}
|
|
241
|
+
|
|
242
|
+
### Checklist
|
|
243
|
+
|
|
244
|
+
- [x] \`skillset.yaml\` validated against schema
|
|
245
|
+
- [x] \`README.md\` with installation and usage instructions
|
|
246
|
+
- [x] \`PROOF.md\` with production evidence
|
|
247
|
+
- [x] \`AUDIT_REPORT.md\` generated and passing
|
|
248
|
+
- [x] \`content/\` directory with skillset files
|
|
249
|
+
|
|
250
|
+
### Notes
|
|
251
|
+
|
|
252
|
+
_Add any additional context for reviewers here._
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
Submitted via \`npx skillsets submit\`
|
|
256
|
+
`;
|
|
257
|
+
const prResult = execSync(`gh pr create --repo ${REGISTRY_REPO} --title "${prTitle}" --body "${prBody.replace(/"/g, '\\"')}"`, { cwd: tempDir, encoding: 'utf-8' });
|
|
258
|
+
// Cleanup
|
|
259
|
+
spinner.text = 'Cleaning up...';
|
|
260
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
261
|
+
spinner.succeed('Pull request created');
|
|
262
|
+
// Extract PR URL from result
|
|
263
|
+
const prUrl = prResult.trim();
|
|
264
|
+
console.log(chalk.green(`\nā ${isUpdate ? 'Update' : 'Submission'} complete!\n`));
|
|
265
|
+
console.log(` Skillset: ${chalk.bold(skillsetId)}`);
|
|
266
|
+
if (isUpdate) {
|
|
267
|
+
console.log(` Version: ${chalk.gray(existingVersion)} ā ${chalk.bold(skillset.version)}`);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
console.log(` Version: ${chalk.bold(skillset.version)}`);
|
|
271
|
+
}
|
|
272
|
+
console.log(` PR: ${chalk.cyan(prUrl)}`);
|
|
273
|
+
console.log('');
|
|
274
|
+
console.log(chalk.gray('A maintainer will review your submission.'));
|
|
275
|
+
console.log(chalk.gray('You can track progress at the PR link above.'));
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
spinner.fail('Submission failed');
|
|
279
|
+
console.log(chalk.red('\nError details:'));
|
|
280
|
+
console.log(chalk.gray(error.message || error));
|
|
281
|
+
// Cleanup on error
|
|
282
|
+
if (existsSync(tempDir)) {
|
|
283
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
284
|
+
}
|
|
285
|
+
console.log(chalk.cyan('\nTo submit manually:'));
|
|
286
|
+
console.log(` 1. Fork ${REGISTRY_URL}`);
|
|
287
|
+
console.log(` 2. Create skillsets/@${skillset.author}/${skillset.name}/`);
|
|
288
|
+
console.log(' 3. Copy your skillset files');
|
|
289
|
+
console.log(' 4. Open a pull request');
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { verifyChecksums } from '../lib/checksum.js';
|
|
4
|
+
import { detectSkillset } from '../lib/filesystem.js';
|
|
5
|
+
export async function verify(options) {
|
|
6
|
+
const dir = options.dir || process.cwd();
|
|
7
|
+
const spinner = ora('Detecting skillset...').start();
|
|
8
|
+
// Detect which skillset is installed
|
|
9
|
+
const skillsetId = await detectSkillset(dir);
|
|
10
|
+
if (!skillsetId) {
|
|
11
|
+
spinner.fail('No skillset.yaml found in directory');
|
|
12
|
+
throw new Error('No skillset.yaml found in directory');
|
|
13
|
+
}
|
|
14
|
+
spinner.text = `Verifying ${skillsetId}...`;
|
|
15
|
+
// Verify checksums
|
|
16
|
+
const result = await verifyChecksums(skillsetId, dir);
|
|
17
|
+
if (result.valid) {
|
|
18
|
+
spinner.succeed('All checksums match!');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
spinner.fail('Checksum verification failed');
|
|
22
|
+
console.log(chalk.yellow('\nMismatched files:'));
|
|
23
|
+
result.mismatches.forEach(({ file, expected, actual }) => {
|
|
24
|
+
console.log(` ${chalk.red('ā')} ${file}`);
|
|
25
|
+
console.log(` Expected: ${expected}`);
|
|
26
|
+
console.log(` Actual: ${actual}`);
|
|
27
|
+
});
|
|
28
|
+
console.log(chalk.cyan('\nTo fix:'));
|
|
29
|
+
console.log(` npx skillsets install ${skillsetId} --force`);
|
|
30
|
+
throw new Error('Checksum verification failed');
|
|
31
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { program } from 'commander';
|
|
3
|
+
import { search } from './commands/search.js';
|
|
4
|
+
import { list } from './commands/list.js';
|
|
5
|
+
import { install } from './commands/install.js';
|
|
6
|
+
import { verify } from './commands/verify.js';
|
|
7
|
+
import { init } from './commands/init.js';
|
|
8
|
+
import { audit } from './commands/audit.js';
|
|
9
|
+
import { submit } from './commands/submit.js';
|
|
10
|
+
import { handleError } from './lib/errors.js';
|
|
11
|
+
program
|
|
12
|
+
.name('skillsets')
|
|
13
|
+
.description('CLI tool for discovering and installing verified skillsets')
|
|
14
|
+
.version('0.1.0');
|
|
15
|
+
// === Discovery Commands ===
|
|
16
|
+
program
|
|
17
|
+
.command('list')
|
|
18
|
+
.description('List all available skillsets')
|
|
19
|
+
.option('-l, --limit <number>', 'Limit results')
|
|
20
|
+
.option('-s, --sort <field>', 'Sort by: name, stars (default: name)')
|
|
21
|
+
.option('--json', 'Output as JSON')
|
|
22
|
+
.action(async (options) => {
|
|
23
|
+
try {
|
|
24
|
+
await list(options);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
handleError(error);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
program
|
|
31
|
+
.command('search')
|
|
32
|
+
.description('Search for skillsets by name, description, or tags')
|
|
33
|
+
.argument('<query>', 'Search query')
|
|
34
|
+
.option('-t, --tags <tags...>', 'Filter by tags')
|
|
35
|
+
.option('-l, --limit <number>', 'Limit results (default: 10)', '10')
|
|
36
|
+
.action(async (query, options) => {
|
|
37
|
+
try {
|
|
38
|
+
await search(query, options);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
handleError(error);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
program
|
|
45
|
+
.command('install')
|
|
46
|
+
.description('Install a skillset to the current directory')
|
|
47
|
+
.argument('<skillsetId>', 'Skillset ID (e.g., @user/skillset-name)')
|
|
48
|
+
.option('-f, --force', 'Overwrite existing files')
|
|
49
|
+
.option('-b, --backup', 'Backup existing files before install')
|
|
50
|
+
.action(async (skillsetId, options) => {
|
|
51
|
+
try {
|
|
52
|
+
await install(skillsetId, options);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
handleError(error);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
program
|
|
59
|
+
.command('verify')
|
|
60
|
+
.description('Verify installed skillset checksums against registry')
|
|
61
|
+
.option('-d, --dir <path>', 'Directory to verify (default: current)', '.')
|
|
62
|
+
.action(async (options) => {
|
|
63
|
+
try {
|
|
64
|
+
await verify(options);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
handleError(error);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
// === Contribution Commands ===
|
|
71
|
+
program
|
|
72
|
+
.command('init')
|
|
73
|
+
.description('Initialize a new skillset submission')
|
|
74
|
+
.option('-y, --yes', 'Accept defaults without prompting')
|
|
75
|
+
.action(async (options) => {
|
|
76
|
+
try {
|
|
77
|
+
await init(options);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
handleError(error);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
program
|
|
84
|
+
.command('audit')
|
|
85
|
+
.description('Validate skillset and generate audit report')
|
|
86
|
+
.action(async () => {
|
|
87
|
+
try {
|
|
88
|
+
await audit();
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
handleError(error);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
program
|
|
95
|
+
.command('submit')
|
|
96
|
+
.description('Submit skillset to registry via GitHub PR')
|
|
97
|
+
.action(async () => {
|
|
98
|
+
try {
|
|
99
|
+
await submit();
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
handleError(error);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
program.parse();
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SearchIndex, SearchIndexEntry } from '../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Fetches the search index from the CDN.
|
|
4
|
+
* Implements 1-hour local cache to reduce network requests.
|
|
5
|
+
*/
|
|
6
|
+
export declare function fetchSearchIndex(): Promise<SearchIndex>;
|
|
7
|
+
/**
|
|
8
|
+
* Fetches metadata for a specific skillset by ID.
|
|
9
|
+
*/
|
|
10
|
+
export declare function fetchSkillsetMetadata(skillsetId: string): Promise<SearchIndexEntry | undefined>;
|
package/dist/lib/api.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { SEARCH_INDEX_URL, CACHE_TTL_MS } from './constants.js';
|
|
2
|
+
let cachedIndex = null;
|
|
3
|
+
let cacheTime = 0;
|
|
4
|
+
/**
|
|
5
|
+
* Fetches the search index from the CDN.
|
|
6
|
+
* Implements 1-hour local cache to reduce network requests.
|
|
7
|
+
*/
|
|
8
|
+
export async function fetchSearchIndex() {
|
|
9
|
+
const now = Date.now();
|
|
10
|
+
// Return cached if still valid
|
|
11
|
+
if (cachedIndex && now - cacheTime < CACHE_TTL_MS) {
|
|
12
|
+
return cachedIndex;
|
|
13
|
+
}
|
|
14
|
+
const response = await fetch(SEARCH_INDEX_URL);
|
|
15
|
+
if (!response.ok) {
|
|
16
|
+
throw new Error(`Failed to fetch search index: ${response.statusText}`);
|
|
17
|
+
}
|
|
18
|
+
const data = (await response.json());
|
|
19
|
+
cachedIndex = data;
|
|
20
|
+
cacheTime = now;
|
|
21
|
+
return data;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Fetches metadata for a specific skillset by ID.
|
|
25
|
+
*/
|
|
26
|
+
export async function fetchSkillsetMetadata(skillsetId) {
|
|
27
|
+
const index = await fetchSearchIndex();
|
|
28
|
+
return index.skillsets.find((s) => s.id === skillsetId);
|
|
29
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Computes SHA-256 checksum for a file.
|
|
3
|
+
*/
|
|
4
|
+
export declare function computeFileChecksum(filePath: string): Promise<string>;
|
|
5
|
+
/**
|
|
6
|
+
* Verifies checksums of installed skillset against registry.
|
|
7
|
+
* Returns validation result with list of mismatches.
|
|
8
|
+
*/
|
|
9
|
+
export declare function verifyChecksums(skillsetId: string, dir: string): Promise<{
|
|
10
|
+
valid: boolean;
|
|
11
|
+
mismatches: Array<{
|
|
12
|
+
file: string;
|
|
13
|
+
expected: string;
|
|
14
|
+
actual: string;
|
|
15
|
+
}>;
|
|
16
|
+
}>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as crypto from 'crypto';
|
|
2
|
+
import * as fs from 'fs/promises';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { fetchSkillsetMetadata } from './api.js';
|
|
5
|
+
/**
|
|
6
|
+
* Computes SHA-256 checksum for a file.
|
|
7
|
+
*/
|
|
8
|
+
export async function computeFileChecksum(filePath) {
|
|
9
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
10
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Verifies checksums of installed skillset against registry.
|
|
14
|
+
* Returns validation result with list of mismatches.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Strips algorithm prefix from checksum (e.g., "sha256:abc123" -> "abc123").
|
|
18
|
+
*/
|
|
19
|
+
function stripChecksumPrefix(checksum) {
|
|
20
|
+
const colonIndex = checksum.indexOf(':');
|
|
21
|
+
return colonIndex !== -1 ? checksum.slice(colonIndex + 1) : checksum;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Verifies checksums of installed skillset against registry.
|
|
25
|
+
* Returns validation result with list of mismatches.
|
|
26
|
+
*/
|
|
27
|
+
export async function verifyChecksums(skillsetId, dir) {
|
|
28
|
+
const metadata = await fetchSkillsetMetadata(skillsetId);
|
|
29
|
+
if (!metadata) {
|
|
30
|
+
throw new Error(`Skillset ${skillsetId} not found in registry`);
|
|
31
|
+
}
|
|
32
|
+
const mismatches = [];
|
|
33
|
+
for (const [file, expectedChecksum] of Object.entries(metadata.files)) {
|
|
34
|
+
const filePath = path.join(dir, file);
|
|
35
|
+
try {
|
|
36
|
+
const actualChecksum = await computeFileChecksum(filePath);
|
|
37
|
+
// Strip sha256: prefix from expected checksum for comparison
|
|
38
|
+
const expectedHex = stripChecksumPrefix(expectedChecksum);
|
|
39
|
+
if (actualChecksum !== expectedHex) {
|
|
40
|
+
mismatches.push({ file, expected: expectedHex, actual: actualChecksum });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
// File missing or unreadable
|
|
45
|
+
mismatches.push({
|
|
46
|
+
file,
|
|
47
|
+
expected: stripChecksumPrefix(expectedChecksum),
|
|
48
|
+
actual: 'MISSING',
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { valid: mismatches.length === 0, mismatches };
|
|
53
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const CDN_BASE_URL = "https://skillsets.cc";
|
|
2
|
+
export declare const SEARCH_INDEX_URL = "https://skillsets.cc/search-index.json";
|
|
3
|
+
export declare const REGISTRY_REPO = "skillsets-cc/main";
|
|
4
|
+
export declare const CACHE_TTL_MS: number;
|
|
5
|
+
export declare const DEFAULT_SEARCH_LIMIT = 10;
|
|
6
|
+
export declare const BACKUP_DIR_NAME = ".claude.backup";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const CDN_BASE_URL = 'https://skillsets.cc';
|
|
2
|
+
export const SEARCH_INDEX_URL = `${CDN_BASE_URL}/search-index.json`;
|
|
3
|
+
export const REGISTRY_REPO = 'skillsets-cc/main';
|
|
4
|
+
export const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
5
|
+
export const DEFAULT_SEARCH_LIMIT = 10;
|
|
6
|
+
export const BACKUP_DIR_NAME = '.claude.backup';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function handleError(error: unknown): void;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
export function handleError(error) {
|
|
3
|
+
if (error instanceof Error) {
|
|
4
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
5
|
+
}
|
|
6
|
+
else {
|
|
7
|
+
console.error(chalk.red('Unexpected error:'));
|
|
8
|
+
console.error(error);
|
|
9
|
+
}
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects conflicts with existing files that would be overwritten during installation.
|
|
3
|
+
*/
|
|
4
|
+
export declare function detectConflicts(dir: string): Promise<string[]>;
|
|
5
|
+
/**
|
|
6
|
+
* Backs up existing files to .claude.backup directory.
|
|
7
|
+
*/
|
|
8
|
+
export declare function backupFiles(files: string[], dir: string): Promise<void>;
|
|
9
|
+
/**
|
|
10
|
+
* Detects which skillset is installed in the given directory.
|
|
11
|
+
* Returns the skillset ID or null if not found.
|
|
12
|
+
*/
|
|
13
|
+
export declare function detectSkillset(dir: string): Promise<string | null>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
|
+
import { BACKUP_DIR_NAME } from './constants.js';
|
|
5
|
+
const CONFLICT_CHECK_PATHS = ['.claude/', 'CLAUDE.md', 'skillset.yaml'];
|
|
6
|
+
/**
|
|
7
|
+
* Detects conflicts with existing files that would be overwritten during installation.
|
|
8
|
+
*/
|
|
9
|
+
export async function detectConflicts(dir) {
|
|
10
|
+
const conflicts = [];
|
|
11
|
+
for (const checkPath of CONFLICT_CHECK_PATHS) {
|
|
12
|
+
const fullPath = path.join(dir, checkPath);
|
|
13
|
+
try {
|
|
14
|
+
await fs.access(fullPath);
|
|
15
|
+
conflicts.push(checkPath);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// File doesn't exist, no conflict
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return conflicts;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Backs up existing files to .claude.backup directory.
|
|
25
|
+
*/
|
|
26
|
+
export async function backupFiles(files, dir) {
|
|
27
|
+
const backupDir = path.join(dir, BACKUP_DIR_NAME);
|
|
28
|
+
await fs.mkdir(backupDir, { recursive: true });
|
|
29
|
+
for (const file of files) {
|
|
30
|
+
const src = path.join(dir, file);
|
|
31
|
+
const dest = path.join(backupDir, file);
|
|
32
|
+
// Create parent directories
|
|
33
|
+
await fs.mkdir(path.dirname(dest), { recursive: true });
|
|
34
|
+
// Copy file or directory recursively
|
|
35
|
+
const stats = await fs.stat(src);
|
|
36
|
+
if (stats.isDirectory()) {
|
|
37
|
+
await copyDirectory(src, dest);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
await fs.copyFile(src, dest);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Recursively copies a directory.
|
|
46
|
+
*/
|
|
47
|
+
async function copyDirectory(src, dest) {
|
|
48
|
+
await fs.mkdir(dest, { recursive: true });
|
|
49
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
const srcPath = path.join(src, entry.name);
|
|
52
|
+
const destPath = path.join(dest, entry.name);
|
|
53
|
+
if (entry.isDirectory()) {
|
|
54
|
+
await copyDirectory(srcPath, destPath);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
await fs.copyFile(srcPath, destPath);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Detects which skillset is installed in the given directory.
|
|
63
|
+
* Returns the skillset ID or null if not found.
|
|
64
|
+
*/
|
|
65
|
+
export async function detectSkillset(dir) {
|
|
66
|
+
try {
|
|
67
|
+
const yamlPath = path.join(dir, 'skillset.yaml');
|
|
68
|
+
const content = await fs.readFile(yamlPath, 'utf-8');
|
|
69
|
+
const data = yaml.load(content);
|
|
70
|
+
return `${data.author.handle}/${data.name}`;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|