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,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,5 @@
1
+ interface VerifyOptions {
2
+ dir?: string;
3
+ }
4
+ export declare function verify(options: VerifyOptions): Promise<void>;
5
+ export {};
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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>;
@@ -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
+ }