skillscokac 1.0.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/LICENSE +15 -0
- package/README.md +235 -0
- package/bin/skillscokac.js +821 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 코드깎는노인 <monogatree@gmail.com>
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# skillscokac
|
|
2
|
+
|
|
3
|
+
CLI tool to install and manage Claude Code skills from [skills.cokac.com](https://skills.cokac.com)
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
No installation required! Use `npx` to run the CLI directly:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx skillscokac [options]
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### Quick Install (Interactive)
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx skillscokac -i <skill-name>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The CLI will:
|
|
22
|
+
1. Fetch the skill from skills.cokac.com
|
|
23
|
+
2. Display skill information (name, description, author, version, files)
|
|
24
|
+
3. Prompt you to choose installation type
|
|
25
|
+
4. Install the skill files
|
|
26
|
+
|
|
27
|
+
### All Commands
|
|
28
|
+
|
|
29
|
+
| Command | Description |
|
|
30
|
+
|---------|-------------|
|
|
31
|
+
| `-i, --install-skill <skillName>` | Install a single skill |
|
|
32
|
+
| `-c, --install-collection <collectionId>` | Install all skills from a collection |
|
|
33
|
+
| `-l, --list-installed-skills` | List all installed skills |
|
|
34
|
+
| `-r, --remove-skill <skillName>` | Remove an installed skill (with confirmation) |
|
|
35
|
+
| `-f, --remove-skill-force <skillName>` | Remove a skill without confirmation |
|
|
36
|
+
| `-a, --remove-all-skills` | Remove all installed skills (with confirmation) |
|
|
37
|
+
| `-A, --remove-all-skills-force` | Remove all skills without confirmation |
|
|
38
|
+
|
|
39
|
+
### Examples
|
|
40
|
+
|
|
41
|
+
**Install a single skill:**
|
|
42
|
+
```bash
|
|
43
|
+
npx skillscokac -i my-awesome-skill
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Install a collection:**
|
|
47
|
+
```bash
|
|
48
|
+
npx skillscokac -c collection-id-here
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**List installed skills:**
|
|
52
|
+
```bash
|
|
53
|
+
npx skillscokac -l
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Remove a skill:**
|
|
57
|
+
```bash
|
|
58
|
+
npx skillscokac -r my-awesome-skill
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Remove all skills:**
|
|
62
|
+
```bash
|
|
63
|
+
npx skillscokac -a
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Installation Locations
|
|
67
|
+
|
|
68
|
+
When installing, you'll be prompted to choose between:
|
|
69
|
+
|
|
70
|
+
### Personal Skills (Global)
|
|
71
|
+
- **Location**: `~/.claude/skills/<skill-name>/`
|
|
72
|
+
- **Scope**: Available in all Claude Code sessions across all projects
|
|
73
|
+
- **Use case**: Skills you want to use everywhere
|
|
74
|
+
|
|
75
|
+
### Project Skills (Local)
|
|
76
|
+
- **Location**: `.claude/skills/<skill-name>/` (in current directory)
|
|
77
|
+
- **Scope**: Available only in the current project
|
|
78
|
+
- **Use case**: Project-specific skills or testing before making them global
|
|
79
|
+
|
|
80
|
+
## Using Installed Skills
|
|
81
|
+
|
|
82
|
+
After installation, use the skill in Claude Code with:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
/<skill-name>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Run this slash command in your Claude Code session to execute the skill.
|
|
89
|
+
|
|
90
|
+
## Collection Installation
|
|
91
|
+
|
|
92
|
+
Collections allow you to install multiple related skills at once:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npx skillscokac -c <collection-id>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The CLI will:
|
|
99
|
+
1. Fetch collection metadata
|
|
100
|
+
2. Display all available skills in the collection
|
|
101
|
+
3. Confirm installation
|
|
102
|
+
4. Prompt for installation type (applies to all skills in collection)
|
|
103
|
+
5. Install all skills with progress feedback
|
|
104
|
+
|
|
105
|
+
## Skill Management
|
|
106
|
+
|
|
107
|
+
### Listing Installed Skills
|
|
108
|
+
|
|
109
|
+
View all installed skills with detailed information:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
npx skillscokac -l
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
This shows:
|
|
116
|
+
- Personal skills (global) with their paths
|
|
117
|
+
- Project skills (local) with their paths
|
|
118
|
+
- Skill names, descriptions, and versions
|
|
119
|
+
- Total count of installed skills
|
|
120
|
+
|
|
121
|
+
### Removing Skills
|
|
122
|
+
|
|
123
|
+
**Interactive removal** (with confirmation):
|
|
124
|
+
```bash
|
|
125
|
+
npx skillscokac -r <skill-name>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
If a skill is installed in both locations, you'll be asked where to remove it from.
|
|
129
|
+
|
|
130
|
+
**Force removal** (no confirmation):
|
|
131
|
+
```bash
|
|
132
|
+
npx skillscokac -f <skill-name>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Removes from all locations without prompting.
|
|
136
|
+
|
|
137
|
+
### Removing All Skills
|
|
138
|
+
|
|
139
|
+
**Interactive removal** (with confirmation):
|
|
140
|
+
```bash
|
|
141
|
+
npx skillscokac -a
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Shows all skills that will be deleted and asks for confirmation.
|
|
145
|
+
|
|
146
|
+
**Force removal** (no confirmation):
|
|
147
|
+
```bash
|
|
148
|
+
npx skillscokac -A
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Immediately removes all skills from all locations.
|
|
152
|
+
|
|
153
|
+
## What Gets Installed
|
|
154
|
+
|
|
155
|
+
When you install a skill, the CLI downloads and extracts:
|
|
156
|
+
- `SKILL.md` - Main skill file with YAML frontmatter metadata
|
|
157
|
+
- Additional files (if any) - Supporting files in their original directory structure
|
|
158
|
+
- All files are extracted from a ZIP package served by the marketplace
|
|
159
|
+
|
|
160
|
+
## Requirements
|
|
161
|
+
|
|
162
|
+
- **Node.js**: 14.0.0 or higher
|
|
163
|
+
- **Claude Code**: Installed and configured
|
|
164
|
+
|
|
165
|
+
## API Endpoints
|
|
166
|
+
|
|
167
|
+
This CLI communicates with the following endpoints:
|
|
168
|
+
|
|
169
|
+
| Endpoint | Purpose |
|
|
170
|
+
|----------|---------|
|
|
171
|
+
| `GET /api/marketplace` | Fetch marketplace data to find skills |
|
|
172
|
+
| `GET /api/posts/{postId}/export-skill-zip` | Download skill ZIP package |
|
|
173
|
+
| `GET /api/collections/{collectionId}` | Fetch collection metadata and skills |
|
|
174
|
+
|
|
175
|
+
**Base URL**: `https://skills.cokac.com`
|
|
176
|
+
|
|
177
|
+
## Development
|
|
178
|
+
|
|
179
|
+
### Local Testing
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
# Clone the repository
|
|
183
|
+
git clone https://github.com/kstost/skillscokac.git
|
|
184
|
+
cd skillscokac
|
|
185
|
+
|
|
186
|
+
# Install dependencies
|
|
187
|
+
npm install
|
|
188
|
+
|
|
189
|
+
# Test locally
|
|
190
|
+
node bin/skillscokac.js -i <skill-name>
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Project Structure
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
skillscokac/
|
|
197
|
+
├── bin/
|
|
198
|
+
│ └── skillscokac.js # Main CLI implementation
|
|
199
|
+
├── package.json # Project metadata and dependencies
|
|
200
|
+
├── package-lock.json # Dependency version lock
|
|
201
|
+
└── README.md # Documentation (this file)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Publishing to npm
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
npm publish
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Technologies Used
|
|
211
|
+
|
|
212
|
+
- **commander** - CLI command parsing and argument handling
|
|
213
|
+
- **axios** - HTTP requests to skills.cokac.com API
|
|
214
|
+
- **inquirer** - Interactive prompts for user input
|
|
215
|
+
- **chalk** - Terminal text styling and colors
|
|
216
|
+
- **ora** - Loading spinners and progress indicators
|
|
217
|
+
- **adm-zip** - ZIP file extraction
|
|
218
|
+
- **boxen** - Terminal UI boxes for skill listings
|
|
219
|
+
- **yaml** - YAML frontmatter parsing from SKILL.md
|
|
220
|
+
|
|
221
|
+
## License
|
|
222
|
+
|
|
223
|
+
ISC
|
|
224
|
+
|
|
225
|
+
## Support
|
|
226
|
+
|
|
227
|
+
For issues, questions, or contributions:
|
|
228
|
+
- Visit [skills.cokac.com](https://skills.cokac.com)
|
|
229
|
+
- GitHub Issues: [Report an issue](https://github.com/kstost/skillscokac/issues)
|
|
230
|
+
|
|
231
|
+
## Author
|
|
232
|
+
|
|
233
|
+
**코드깎는노인** <monogatree@gmail.com>
|
|
234
|
+
|
|
235
|
+
Website: [skills.cokac.com](https://skills.cokac.com)
|
|
@@ -0,0 +1,821 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander')
|
|
4
|
+
const axios = require('axios')
|
|
5
|
+
const inquirer = require('inquirer')
|
|
6
|
+
const fs = require('fs')
|
|
7
|
+
const path = require('path')
|
|
8
|
+
const os = require('os')
|
|
9
|
+
const chalk = require('chalk')
|
|
10
|
+
const ora = require('ora')
|
|
11
|
+
const yaml = require('yaml')
|
|
12
|
+
const AdmZip = require('adm-zip')
|
|
13
|
+
const boxen = require('boxen')
|
|
14
|
+
|
|
15
|
+
// Load package.json for version
|
|
16
|
+
const packageJson = require(path.join(__dirname, '..', 'package.json'))
|
|
17
|
+
const VERSION = packageJson.version
|
|
18
|
+
|
|
19
|
+
const API_BASE_URL = 'https://skills.cokac.com'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parse frontmatter from markdown content
|
|
23
|
+
*/
|
|
24
|
+
function parseFrontmatter(content) {
|
|
25
|
+
const frontmatterRegex = /^---\n([\s\S]*?)\n---/
|
|
26
|
+
const match = content.match(frontmatterRegex)
|
|
27
|
+
|
|
28
|
+
if (!match) {
|
|
29
|
+
return { metadata: {}, content }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const metadata = yaml.parse(match[1])
|
|
34
|
+
const markdownContent = content.slice(match[0].length).trim()
|
|
35
|
+
return { metadata, content: markdownContent }
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error(chalk.red('Error parsing frontmatter:'), error.message)
|
|
38
|
+
return { metadata: {}, content }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Display skill information
|
|
44
|
+
*/
|
|
45
|
+
function displaySkillInfo(skill) {
|
|
46
|
+
const { metadata } = parseFrontmatter(skill.skillMd || '')
|
|
47
|
+
|
|
48
|
+
// Skill name
|
|
49
|
+
const displayName = metadata.name || skill.skillName
|
|
50
|
+
console.log(chalk.bold.cyan(`/${skill.skillName}`) + (displayName !== skill.skillName ? chalk.dim(` (${displayName})`) : ''))
|
|
51
|
+
|
|
52
|
+
// Description
|
|
53
|
+
if (metadata.description) {
|
|
54
|
+
console.log(chalk.dim(metadata.description))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Metadata in compact format
|
|
58
|
+
const metaItems = []
|
|
59
|
+
|
|
60
|
+
if (skill.author) {
|
|
61
|
+
metaItems.push(chalk.dim('Author: ') + chalk.white(skill.author.name || skill.author.username))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const totalFiles = 1 + (skill.files ? skill.files.length : 0)
|
|
65
|
+
metaItems.push(chalk.dim('Files: ') + chalk.white(`${totalFiles} file${totalFiles !== 1 ? 's' : ''}`))
|
|
66
|
+
|
|
67
|
+
if (skill.version) {
|
|
68
|
+
metaItems.push(chalk.dim('Version: ') + chalk.white(skill.version))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log(' ' + metaItems.join(chalk.dim(' • ')))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Fetch skill from Marketplace and download ZIP
|
|
76
|
+
*/
|
|
77
|
+
async function fetchSkill(skillName, options = {}) {
|
|
78
|
+
const silent = options.silent || false
|
|
79
|
+
const spinner = silent ? null : ora(`Searching for skill: ${skillName}`).start()
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
// Step 1: Get marketplace data to find postId
|
|
83
|
+
if (spinner) spinner.text = 'Fetching marketplace data...'
|
|
84
|
+
const marketplaceResponse = await axios.get(`${API_BASE_URL}/api/marketplace`, {
|
|
85
|
+
headers: {
|
|
86
|
+
'User-Agent': `skillscokac-cli/${VERSION}`
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const marketplace = marketplaceResponse.data
|
|
91
|
+
|
|
92
|
+
// Validate marketplace data structure
|
|
93
|
+
if (!marketplace || typeof marketplace !== 'object') {
|
|
94
|
+
throw new Error('Invalid marketplace data received')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!Array.isArray(marketplace.plugins)) {
|
|
98
|
+
throw new Error('Invalid marketplace data: plugins not found')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const plugin = marketplace.plugins.find(p => p && p.name === skillName)
|
|
102
|
+
|
|
103
|
+
if (!plugin) {
|
|
104
|
+
throw new Error(`Skill "${skillName}" not found`)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Validate plugin structure
|
|
108
|
+
if (!plugin.source || !plugin.source.url) {
|
|
109
|
+
throw new Error('Invalid skill data: missing source URL')
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Extract postId from plugin URL
|
|
113
|
+
const postIdMatch = plugin.source.url.match(/\/plugins\/([^/.]+)/)
|
|
114
|
+
if (!postIdMatch) {
|
|
115
|
+
throw new Error('Failed to parse skill URL')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const postId = postIdMatch[1]
|
|
119
|
+
|
|
120
|
+
// Step 2: Download ZIP file
|
|
121
|
+
if (spinner) spinner.text = 'Downloading skill files...'
|
|
122
|
+
const zipResponse = await axios.get(
|
|
123
|
+
`${API_BASE_URL}/api/posts/${postId}/export-skill-zip`,
|
|
124
|
+
{
|
|
125
|
+
responseType: 'arraybuffer',
|
|
126
|
+
headers: {
|
|
127
|
+
'User-Agent': `skillscokac-cli/${VERSION}`
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
// Step 3: Extract ZIP
|
|
133
|
+
if (spinner) spinner.text = 'Extracting files...'
|
|
134
|
+
const zip = new AdmZip(Buffer.from(zipResponse.data))
|
|
135
|
+
const zipEntries = zip.getEntries()
|
|
136
|
+
|
|
137
|
+
// Find SKILL.md
|
|
138
|
+
const skillMdEntry = zipEntries.find(entry =>
|
|
139
|
+
entry.entryName.endsWith('SKILL.md') && !entry.isDirectory
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
if (!skillMdEntry) {
|
|
143
|
+
throw new Error('Invalid skill package')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const skillMdContent = skillMdEntry.getData().toString('utf8')
|
|
147
|
+
const { metadata } = parseFrontmatter(skillMdContent)
|
|
148
|
+
|
|
149
|
+
// Get additional files (excluding SKILL.md)
|
|
150
|
+
const additionalFiles = zipEntries
|
|
151
|
+
.filter(entry =>
|
|
152
|
+
!entry.isDirectory &&
|
|
153
|
+
!entry.entryName.endsWith('SKILL.md') &&
|
|
154
|
+
!entry.entryName.includes('.claude-plugin/')
|
|
155
|
+
)
|
|
156
|
+
.map(entry => {
|
|
157
|
+
const fullPath = entry.entryName
|
|
158
|
+
const skillFolder = `${skillName}/`
|
|
159
|
+
const relativePath = fullPath.startsWith(skillFolder)
|
|
160
|
+
? fullPath.substring(skillFolder.length)
|
|
161
|
+
: fullPath
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
path: relativePath,
|
|
165
|
+
filename: path.basename(relativePath),
|
|
166
|
+
content: entry.getData().toString('utf8')
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
if (spinner) spinner.stop()
|
|
171
|
+
|
|
172
|
+
// Construct skill object
|
|
173
|
+
return {
|
|
174
|
+
id: postId,
|
|
175
|
+
skillName: skillName,
|
|
176
|
+
description: plugin.description || metadata.description,
|
|
177
|
+
version: metadata.version, // Only show if explicitly defined
|
|
178
|
+
skillMd: skillMdContent,
|
|
179
|
+
author: plugin.author,
|
|
180
|
+
files: additionalFiles,
|
|
181
|
+
_zipData: zip // Keep zip for installation
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
} catch (error) {
|
|
185
|
+
if (!silent) {
|
|
186
|
+
if (spinner) spinner.stop()
|
|
187
|
+
console.log(chalk.red(`✖ ${error.message}`))
|
|
188
|
+
process.exit(1)
|
|
189
|
+
}
|
|
190
|
+
throw error
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Prompt for installation type
|
|
196
|
+
*/
|
|
197
|
+
async function promptInstallationType() {
|
|
198
|
+
const answers = await inquirer.prompt([
|
|
199
|
+
{
|
|
200
|
+
type: 'list',
|
|
201
|
+
name: 'installType',
|
|
202
|
+
message: 'Where would you like to install this skill?',
|
|
203
|
+
choices: [
|
|
204
|
+
{
|
|
205
|
+
name: 'Personal Skills (available globally in all projects)',
|
|
206
|
+
value: 'personal',
|
|
207
|
+
short: 'Personal'
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: 'Project Skills (available only in this project)',
|
|
211
|
+
value: 'project',
|
|
212
|
+
short: 'Project'
|
|
213
|
+
}
|
|
214
|
+
]
|
|
215
|
+
}
|
|
216
|
+
])
|
|
217
|
+
|
|
218
|
+
return answers.installType
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Get installation directory based on type
|
|
223
|
+
*/
|
|
224
|
+
function getInstallDirectory(installType, skillName) {
|
|
225
|
+
if (installType === 'personal') {
|
|
226
|
+
return path.join(os.homedir(), '.claude', 'skills', skillName)
|
|
227
|
+
} else {
|
|
228
|
+
return path.join(process.cwd(), '.claude', 'skills', skillName)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Install skill files
|
|
234
|
+
*/
|
|
235
|
+
async function installSkill(skill, installType, options = {}) {
|
|
236
|
+
const installDir = getInstallDirectory(installType, skill.skillName)
|
|
237
|
+
const silent = options.silent || false
|
|
238
|
+
const spinner = silent ? null : ora('Installing skill...').start()
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
// Remove existing directory if it exists
|
|
242
|
+
if (fs.existsSync(installDir)) {
|
|
243
|
+
if (spinner) spinner.text = 'Removing existing skill...'
|
|
244
|
+
fs.rmSync(installDir, { recursive: true, force: true })
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Create fresh directory
|
|
248
|
+
if (spinner) spinner.text = 'Installing skill...'
|
|
249
|
+
fs.mkdirSync(installDir, { recursive: true })
|
|
250
|
+
|
|
251
|
+
// Write SKILL.md
|
|
252
|
+
const skillMdPath = path.join(installDir, 'SKILL.md')
|
|
253
|
+
fs.writeFileSync(skillMdPath, skill.skillMd || '', 'utf8')
|
|
254
|
+
|
|
255
|
+
// Write additional files
|
|
256
|
+
if (skill.files && skill.files.length > 0) {
|
|
257
|
+
for (const file of skill.files) {
|
|
258
|
+
// Security: Prevent path traversal attacks
|
|
259
|
+
// Normalize the path and remove any leading ../ sequences
|
|
260
|
+
const normalizedPath = path.normalize(file.path).replace(/^(\.\.(\/|\\|$))+/, '')
|
|
261
|
+
const filePath = path.join(installDir, normalizedPath)
|
|
262
|
+
|
|
263
|
+
// Verify that the final path is still within installDir
|
|
264
|
+
const resolvedFilePath = path.resolve(filePath)
|
|
265
|
+
const resolvedInstallDir = path.resolve(installDir)
|
|
266
|
+
|
|
267
|
+
if (!resolvedFilePath.startsWith(resolvedInstallDir + path.sep) && resolvedFilePath !== resolvedInstallDir) {
|
|
268
|
+
console.warn(chalk.yellow(`⚠ Skipping potentially unsafe file path: ${file.path}`))
|
|
269
|
+
continue
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const fileDir = path.dirname(filePath)
|
|
273
|
+
|
|
274
|
+
// Create subdirectories if needed
|
|
275
|
+
if (!fs.existsSync(fileDir)) {
|
|
276
|
+
fs.mkdirSync(fileDir, { recursive: true })
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
fs.writeFileSync(filePath, file.content || '', 'utf8')
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (silent) {
|
|
284
|
+
// Compact one-line output for collection installs
|
|
285
|
+
console.log(chalk.green(' ✓') + chalk.cyan(` /${skill.skillName}`) + chalk.dim(' installed'))
|
|
286
|
+
} else {
|
|
287
|
+
// Detailed output for single skill installs
|
|
288
|
+
spinner.succeed(chalk.green('Installed successfully!'))
|
|
289
|
+
console.log(chalk.dim(' Location: ') + chalk.cyan(installDir))
|
|
290
|
+
|
|
291
|
+
const scope = installType === 'personal' ? 'available globally' : 'available in this project'
|
|
292
|
+
console.log(chalk.dim(' Scope: ') + chalk.white(scope))
|
|
293
|
+
console.log(chalk.bold('Usage: ') + chalk.cyan(`/${skill.skillName}`))
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
} catch (error) {
|
|
297
|
+
if (silent) {
|
|
298
|
+
console.log(chalk.red(' ✗') + chalk.cyan(` /${skill.skillName}`) + chalk.dim(' failed'))
|
|
299
|
+
throw error
|
|
300
|
+
} else {
|
|
301
|
+
spinner.fail(chalk.red('Failed to install skill'))
|
|
302
|
+
console.error(chalk.red('\nError:'), error.message)
|
|
303
|
+
process.exit(1)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Install skill command handler
|
|
310
|
+
*/
|
|
311
|
+
async function installSkillCommand(skillName) {
|
|
312
|
+
// Fetch skill
|
|
313
|
+
const skill = await fetchSkill(skillName)
|
|
314
|
+
|
|
315
|
+
// Display skill info
|
|
316
|
+
displaySkillInfo(skill)
|
|
317
|
+
|
|
318
|
+
// Prompt for installation type
|
|
319
|
+
const installType = await promptInstallationType()
|
|
320
|
+
|
|
321
|
+
// Install skill
|
|
322
|
+
await installSkill(skill, installType)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Install collection command handler
|
|
327
|
+
*/
|
|
328
|
+
async function installCollectionCommand(collectionId) {
|
|
329
|
+
const spinner = ora('Fetching collection...').start()
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
// Fetch collection
|
|
333
|
+
const response = await axios.get(`${API_BASE_URL}/api/collections/${collectionId}`, {
|
|
334
|
+
headers: {
|
|
335
|
+
'User-Agent': `skillscokac-cli/${VERSION}`
|
|
336
|
+
}
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
const collection = response.data
|
|
340
|
+
spinner.stop()
|
|
341
|
+
|
|
342
|
+
console.log(chalk.bold.cyan('Collection:'), collection.name)
|
|
343
|
+
if (collection.description) {
|
|
344
|
+
console.log(chalk.dim(collection.description))
|
|
345
|
+
}
|
|
346
|
+
console.log()
|
|
347
|
+
|
|
348
|
+
// Filter SKILL type posts
|
|
349
|
+
const skills = collection.saves
|
|
350
|
+
.map(save => save.post)
|
|
351
|
+
.filter(post => post && post.type === 'SKILL' && post.skillName && !post.isDeleted)
|
|
352
|
+
|
|
353
|
+
if (skills.length === 0) {
|
|
354
|
+
console.log(chalk.yellow('No skills found in this collection'))
|
|
355
|
+
console.log()
|
|
356
|
+
return
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
console.log(chalk.bold(`Found ${skills.length} skill${skills.length !== 1 ? 's' : ''}:`))
|
|
360
|
+
skills.forEach((skill, index) => {
|
|
361
|
+
console.log(chalk.dim(` ${index + 1}. `) + chalk.cyan(`/${skill.skillName}`) + (skill.description ? chalk.dim(` - ${skill.description}`) : ''))
|
|
362
|
+
})
|
|
363
|
+
console.log()
|
|
364
|
+
|
|
365
|
+
// Confirm installation
|
|
366
|
+
const confirmation = await inquirer.prompt([
|
|
367
|
+
{
|
|
368
|
+
type: 'confirm',
|
|
369
|
+
name: 'confirmInstall',
|
|
370
|
+
message: `Install all ${skills.length} skill${skills.length !== 1 ? 's' : ''}?`,
|
|
371
|
+
default: true
|
|
372
|
+
}
|
|
373
|
+
])
|
|
374
|
+
|
|
375
|
+
if (!confirmation.confirmInstall) {
|
|
376
|
+
console.log(chalk.yellow('Installation cancelled'))
|
|
377
|
+
console.log()
|
|
378
|
+
return
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Prompt for installation type (once for all skills)
|
|
382
|
+
const installType = await promptInstallationType()
|
|
383
|
+
|
|
384
|
+
console.log()
|
|
385
|
+
console.log(chalk.bold('Installing skills...'))
|
|
386
|
+
console.log()
|
|
387
|
+
|
|
388
|
+
let successCount = 0
|
|
389
|
+
let failCount = 0
|
|
390
|
+
|
|
391
|
+
// Install each skill
|
|
392
|
+
for (let i = 0; i < skills.length; i++) {
|
|
393
|
+
const skillPost = skills[i]
|
|
394
|
+
const skillName = skillPost.skillName
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
// Fetch and install skill in silent mode
|
|
398
|
+
const skill = await fetchSkill(skillName, { silent: true })
|
|
399
|
+
await installSkill(skill, installType, { silent: true })
|
|
400
|
+
|
|
401
|
+
successCount++
|
|
402
|
+
} catch (error) {
|
|
403
|
+
console.log(chalk.red(' ✗') + chalk.cyan(` /${skillName}`) + chalk.dim(' failed') + chalk.red(` - ${error.message}`))
|
|
404
|
+
failCount++
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
console.log()
|
|
409
|
+
console.log(chalk.bold('Installation Summary:'))
|
|
410
|
+
console.log(chalk.green(` ✓ Successfully installed: ${successCount}`))
|
|
411
|
+
if (failCount > 0) {
|
|
412
|
+
console.log(chalk.red(` ✗ Failed: ${failCount}`))
|
|
413
|
+
}
|
|
414
|
+
console.log()
|
|
415
|
+
|
|
416
|
+
} catch (error) {
|
|
417
|
+
if (error.response && error.response.status === 404) {
|
|
418
|
+
spinner.fail(chalk.red('Collection not found'))
|
|
419
|
+
} else if (error.response && error.response.status === 403) {
|
|
420
|
+
spinner.fail(chalk.red('Collection is private'))
|
|
421
|
+
} else {
|
|
422
|
+
spinner.fail(chalk.red('Failed to fetch collection'))
|
|
423
|
+
}
|
|
424
|
+
process.exit(1)
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Get skills from a directory
|
|
430
|
+
*/
|
|
431
|
+
function getSkillsFromDirectory(skillsDir) {
|
|
432
|
+
if (!fs.existsSync(skillsDir)) {
|
|
433
|
+
return []
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const skills = []
|
|
437
|
+
const entries = fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
438
|
+
|
|
439
|
+
for (const entry of entries) {
|
|
440
|
+
if (!entry.isDirectory()) continue
|
|
441
|
+
|
|
442
|
+
const skillDir = path.join(skillsDir, entry.name)
|
|
443
|
+
const skillMdPath = path.join(skillDir, 'SKILL.md')
|
|
444
|
+
|
|
445
|
+
if (!fs.existsSync(skillMdPath)) continue
|
|
446
|
+
|
|
447
|
+
try {
|
|
448
|
+
const skillMdContent = fs.readFileSync(skillMdPath, 'utf8')
|
|
449
|
+
const { metadata } = parseFrontmatter(skillMdContent)
|
|
450
|
+
|
|
451
|
+
skills.push({
|
|
452
|
+
name: entry.name,
|
|
453
|
+
displayName: metadata.name || entry.name,
|
|
454
|
+
description: metadata.description || 'No description',
|
|
455
|
+
version: metadata.version,
|
|
456
|
+
path: skillDir
|
|
457
|
+
})
|
|
458
|
+
} catch (error) {
|
|
459
|
+
// Skip if error reading or parsing
|
|
460
|
+
continue
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return skills
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Remove skill command handler
|
|
469
|
+
*/
|
|
470
|
+
async function removeSkillCommand(skillName, force = false) {
|
|
471
|
+
// Check if skill exists in personal and/or project directories
|
|
472
|
+
const personalSkillDir = path.join(os.homedir(), '.claude', 'skills', skillName)
|
|
473
|
+
const projectSkillDir = path.join(process.cwd(), '.claude', 'skills', skillName)
|
|
474
|
+
|
|
475
|
+
const personalExists = fs.existsSync(personalSkillDir)
|
|
476
|
+
const projectExists = fs.existsSync(projectSkillDir)
|
|
477
|
+
|
|
478
|
+
if (!personalExists && !projectExists) {
|
|
479
|
+
console.log(chalk.red(`✗ Skill "${skillName}" is not installed`))
|
|
480
|
+
process.exit(1)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
let dirsToRemove = []
|
|
484
|
+
|
|
485
|
+
if (force) {
|
|
486
|
+
// Force mode: remove from both locations without asking
|
|
487
|
+
if (personalExists) {
|
|
488
|
+
dirsToRemove.push({ dir: personalSkillDir, type: 'personal' })
|
|
489
|
+
}
|
|
490
|
+
if (projectExists) {
|
|
491
|
+
dirsToRemove.push({ dir: projectSkillDir, type: 'project' })
|
|
492
|
+
}
|
|
493
|
+
} else {
|
|
494
|
+
// Normal mode: ask where to remove from
|
|
495
|
+
if (personalExists && projectExists) {
|
|
496
|
+
const answer = await inquirer.prompt([
|
|
497
|
+
{
|
|
498
|
+
type: 'list',
|
|
499
|
+
name: 'removeFrom',
|
|
500
|
+
message: `Skill "${skillName}" is installed in both locations. Where do you want to remove it from?`,
|
|
501
|
+
choices: [
|
|
502
|
+
{
|
|
503
|
+
name: 'Personal Skills (global)',
|
|
504
|
+
value: 'personal',
|
|
505
|
+
short: 'Personal'
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
name: 'Project Skills (local)',
|
|
509
|
+
value: 'project',
|
|
510
|
+
short: 'Project'
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
name: 'Both locations',
|
|
514
|
+
value: 'both',
|
|
515
|
+
short: 'Both'
|
|
516
|
+
}
|
|
517
|
+
]
|
|
518
|
+
}
|
|
519
|
+
])
|
|
520
|
+
|
|
521
|
+
if (answer.removeFrom === 'personal') {
|
|
522
|
+
dirsToRemove = [{ dir: personalSkillDir, type: 'personal' }]
|
|
523
|
+
} else if (answer.removeFrom === 'project') {
|
|
524
|
+
dirsToRemove = [{ dir: projectSkillDir, type: 'project' }]
|
|
525
|
+
} else {
|
|
526
|
+
dirsToRemove = [
|
|
527
|
+
{ dir: personalSkillDir, type: 'personal' },
|
|
528
|
+
{ dir: projectSkillDir, type: 'project' }
|
|
529
|
+
]
|
|
530
|
+
}
|
|
531
|
+
} else if (personalExists) {
|
|
532
|
+
dirsToRemove = [{ dir: personalSkillDir, type: 'personal' }]
|
|
533
|
+
} else {
|
|
534
|
+
dirsToRemove = [{ dir: projectSkillDir, type: 'project' }]
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Confirm deletion
|
|
538
|
+
const confirmation = await inquirer.prompt([
|
|
539
|
+
{
|
|
540
|
+
type: 'confirm',
|
|
541
|
+
name: 'confirmDelete',
|
|
542
|
+
message: `Are you sure you want to remove "${skillName}"?`,
|
|
543
|
+
default: false
|
|
544
|
+
}
|
|
545
|
+
])
|
|
546
|
+
|
|
547
|
+
if (!confirmation.confirmDelete) {
|
|
548
|
+
console.log(chalk.yellow('Removal cancelled'))
|
|
549
|
+
console.log()
|
|
550
|
+
return
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Remove the skill(s)
|
|
555
|
+
const spinner = ora('Removing skill...').start()
|
|
556
|
+
|
|
557
|
+
try {
|
|
558
|
+
for (const { dir, type } of dirsToRemove) {
|
|
559
|
+
fs.rmSync(dir, { recursive: true, force: true })
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
spinner.succeed(chalk.green('Skill removed successfully!'))
|
|
563
|
+
console.log()
|
|
564
|
+
|
|
565
|
+
dirsToRemove.forEach(({ type }) => {
|
|
566
|
+
const location = type === 'personal' ? 'Personal Skills' : 'Project Skills'
|
|
567
|
+
console.log(chalk.dim(` ✓ Removed from ${location}`))
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
console.log()
|
|
571
|
+
} catch (error) {
|
|
572
|
+
spinner.fail(chalk.red('Failed to remove skill'))
|
|
573
|
+
console.error(chalk.red('\nError:'), error.message)
|
|
574
|
+
process.exit(1)
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Remove all skills command handler
|
|
580
|
+
*/
|
|
581
|
+
async function removeAllSkillsCommand(force = false) {
|
|
582
|
+
// Get all installed skills
|
|
583
|
+
const personalSkillsDir = path.join(os.homedir(), '.claude', 'skills')
|
|
584
|
+
const projectSkillsDir = path.join(process.cwd(), '.claude', 'skills')
|
|
585
|
+
|
|
586
|
+
const personalSkills = getSkillsFromDirectory(personalSkillsDir)
|
|
587
|
+
const projectSkills = getSkillsFromDirectory(projectSkillsDir)
|
|
588
|
+
|
|
589
|
+
const totalSkills = personalSkills.length + projectSkills.length
|
|
590
|
+
|
|
591
|
+
if (totalSkills === 0) {
|
|
592
|
+
console.log(chalk.yellow('No skills installed'))
|
|
593
|
+
console.log()
|
|
594
|
+
return
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (!force) {
|
|
598
|
+
// Show what will be deleted
|
|
599
|
+
console.log(chalk.bold('Skills to be removed:'))
|
|
600
|
+
console.log()
|
|
601
|
+
|
|
602
|
+
if (personalSkills.length > 0) {
|
|
603
|
+
console.log(chalk.green('Personal Skills:'))
|
|
604
|
+
personalSkills.forEach(skill => {
|
|
605
|
+
console.log(chalk.dim(` - /${skill.name}`))
|
|
606
|
+
})
|
|
607
|
+
console.log()
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (projectSkills.length > 0) {
|
|
611
|
+
console.log(chalk.yellow('Project Skills:'))
|
|
612
|
+
projectSkills.forEach(skill => {
|
|
613
|
+
console.log(chalk.dim(` - /${skill.name}`))
|
|
614
|
+
})
|
|
615
|
+
console.log()
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
console.log(chalk.bold.red(`Total: ${totalSkills} skill${totalSkills !== 1 ? 's' : ''} will be deleted`))
|
|
619
|
+
console.log()
|
|
620
|
+
|
|
621
|
+
// Confirm deletion
|
|
622
|
+
const confirmation = await inquirer.prompt([
|
|
623
|
+
{
|
|
624
|
+
type: 'confirm',
|
|
625
|
+
name: 'confirmDelete',
|
|
626
|
+
message: chalk.red('Are you absolutely sure you want to remove ALL skills?'),
|
|
627
|
+
default: false
|
|
628
|
+
}
|
|
629
|
+
])
|
|
630
|
+
|
|
631
|
+
if (!confirmation.confirmDelete) {
|
|
632
|
+
console.log(chalk.yellow('Removal cancelled'))
|
|
633
|
+
console.log()
|
|
634
|
+
return
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Remove all skills
|
|
639
|
+
const spinner = ora('Removing all skills...').start()
|
|
640
|
+
|
|
641
|
+
try {
|
|
642
|
+
let removedCount = 0
|
|
643
|
+
|
|
644
|
+
// Remove personal skills
|
|
645
|
+
if (personalSkills.length > 0 && fs.existsSync(personalSkillsDir)) {
|
|
646
|
+
for (const skill of personalSkills) {
|
|
647
|
+
const skillDir = path.join(personalSkillsDir, skill.name)
|
|
648
|
+
if (fs.existsSync(skillDir)) {
|
|
649
|
+
fs.rmSync(skillDir, { recursive: true, force: true })
|
|
650
|
+
removedCount++
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Remove project skills
|
|
656
|
+
if (projectSkills.length > 0 && fs.existsSync(projectSkillsDir)) {
|
|
657
|
+
for (const skill of projectSkills) {
|
|
658
|
+
const skillDir = path.join(projectSkillsDir, skill.name)
|
|
659
|
+
if (fs.existsSync(skillDir)) {
|
|
660
|
+
fs.rmSync(skillDir, { recursive: true, force: true })
|
|
661
|
+
removedCount++
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
spinner.succeed(chalk.green('All skills removed successfully!'))
|
|
667
|
+
console.log()
|
|
668
|
+
console.log(chalk.dim(` ✓ Removed ${removedCount} skill${removedCount !== 1 ? 's' : ''}`))
|
|
669
|
+
console.log()
|
|
670
|
+
|
|
671
|
+
} catch (error) {
|
|
672
|
+
spinner.fail(chalk.red('Failed to remove all skills'))
|
|
673
|
+
console.error(chalk.red('\nError:'), error.message)
|
|
674
|
+
process.exit(1)
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* List installed skills command handler
|
|
680
|
+
*/
|
|
681
|
+
async function listInstalledSkillsCommand() {
|
|
682
|
+
// Get personal skills
|
|
683
|
+
const personalSkillsDir = path.join(os.homedir(), '.claude', 'skills')
|
|
684
|
+
const personalSkills = getSkillsFromDirectory(personalSkillsDir)
|
|
685
|
+
|
|
686
|
+
// Get project skills
|
|
687
|
+
const projectSkillsDir = path.join(process.cwd(), '.claude', 'skills')
|
|
688
|
+
const projectSkills = getSkillsFromDirectory(projectSkillsDir)
|
|
689
|
+
|
|
690
|
+
// Display personal skills
|
|
691
|
+
console.log(chalk.bold.green('📦 Personal Skills') + chalk.dim(` (global)`))
|
|
692
|
+
console.log(chalk.dim(` ${personalSkillsDir}`))
|
|
693
|
+
|
|
694
|
+
if (personalSkills.length > 0) {
|
|
695
|
+
const skillContents = personalSkills.map(skill => {
|
|
696
|
+
let content = chalk.bold.cyan(`/${skill.name}`)
|
|
697
|
+
|
|
698
|
+
if (skill.description) {
|
|
699
|
+
content += '\n' + chalk.white(skill.description)
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if (skill.version) {
|
|
703
|
+
content += '\n' + chalk.dim(`Version: ${skill.version}`)
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
return content
|
|
707
|
+
})
|
|
708
|
+
|
|
709
|
+
const allContent = skillContents.join('\n\n')
|
|
710
|
+
|
|
711
|
+
console.log(boxen(allContent, {
|
|
712
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
713
|
+
margin: { top: 0, bottom: 0, left: 2, right: 0 },
|
|
714
|
+
borderStyle: 'round',
|
|
715
|
+
borderColor: 'cyan',
|
|
716
|
+
width: 70
|
|
717
|
+
}))
|
|
718
|
+
console.log()
|
|
719
|
+
} else {
|
|
720
|
+
console.log(chalk.dim(' No personal skills installed'))
|
|
721
|
+
console.log()
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Display project skills
|
|
725
|
+
console.log(chalk.bold.yellow('📁 Project Skills') + chalk.dim(` (current directory)`))
|
|
726
|
+
console.log(chalk.dim(` ${projectSkillsDir}`))
|
|
727
|
+
|
|
728
|
+
if (projectSkills.length > 0) {
|
|
729
|
+
const skillContents = projectSkills.map(skill => {
|
|
730
|
+
let content = chalk.bold.cyan(`/${skill.name}`)
|
|
731
|
+
|
|
732
|
+
if (skill.description) {
|
|
733
|
+
content += '\n' + chalk.white(skill.description)
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
if (skill.version) {
|
|
737
|
+
content += '\n' + chalk.dim(`Version: ${skill.version}`)
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
return content
|
|
741
|
+
})
|
|
742
|
+
|
|
743
|
+
const allContent = skillContents.join('\n\n')
|
|
744
|
+
|
|
745
|
+
console.log(boxen(allContent, {
|
|
746
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
747
|
+
margin: { top: 0, bottom: 0, left: 2, right: 0 },
|
|
748
|
+
borderStyle: 'round',
|
|
749
|
+
borderColor: 'yellow',
|
|
750
|
+
width: 70
|
|
751
|
+
}))
|
|
752
|
+
console.log()
|
|
753
|
+
} else {
|
|
754
|
+
console.log(chalk.dim(' No project skills installed'))
|
|
755
|
+
console.log()
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Summary
|
|
759
|
+
const total = personalSkills.length + projectSkills.length
|
|
760
|
+
if (total === 0) {
|
|
761
|
+
console.log(chalk.dim('No skills installed yet. Install one with:'))
|
|
762
|
+
console.log(chalk.cyan(' npx skillscokac --install-skill <skill-name>'))
|
|
763
|
+
console.log()
|
|
764
|
+
} else {
|
|
765
|
+
console.log(chalk.dim(`Total: ${total} skill${total !== 1 ? 's' : ''} installed`))
|
|
766
|
+
console.log()
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Setup CLI with Commander
|
|
772
|
+
*/
|
|
773
|
+
const program = new Command()
|
|
774
|
+
|
|
775
|
+
program
|
|
776
|
+
.name('skillscokac')
|
|
777
|
+
.description('CLI tool to install Claude Code skills from skills.cokac.com')
|
|
778
|
+
.version(VERSION)
|
|
779
|
+
|
|
780
|
+
program
|
|
781
|
+
.option('-i, --install-skill <skillName>', 'Install a skill by name')
|
|
782
|
+
.option('-c, --install-collection <collectionId>', 'Install all skills from a collection')
|
|
783
|
+
.option('-r, --remove-skill <skillName>', 'Remove an installed skill')
|
|
784
|
+
.option('-f, --remove-skill-force <skillName>', 'Remove skill from all locations without confirmation')
|
|
785
|
+
.option('-a, --remove-all-skills', 'Remove all installed skills')
|
|
786
|
+
.option('-A, --remove-all-skills-force', 'Remove all installed skills without confirmation')
|
|
787
|
+
.option('-l, --list-installed-skills', 'List all installed skills')
|
|
788
|
+
.parse(process.argv)
|
|
789
|
+
|
|
790
|
+
const options = program.opts()
|
|
791
|
+
|
|
792
|
+
// Execute command with proper async/await error handling
|
|
793
|
+
;(async () => {
|
|
794
|
+
try {
|
|
795
|
+
if (options.installSkill) {
|
|
796
|
+
await installSkillCommand(options.installSkill)
|
|
797
|
+
} else if (options.installCollection) {
|
|
798
|
+
await installCollectionCommand(options.installCollection)
|
|
799
|
+
} else if (options.removeAllSkillsForce) {
|
|
800
|
+
await removeAllSkillsCommand(true)
|
|
801
|
+
} else if (options.removeAllSkills) {
|
|
802
|
+
await removeAllSkillsCommand()
|
|
803
|
+
} else if (options.removeSkillForce) {
|
|
804
|
+
await removeSkillCommand(options.removeSkillForce, true)
|
|
805
|
+
} else if (options.removeSkill) {
|
|
806
|
+
await removeSkillCommand(options.removeSkill)
|
|
807
|
+
} else if (options.listInstalledSkills) {
|
|
808
|
+
await listInstalledSkillsCommand()
|
|
809
|
+
} else {
|
|
810
|
+
// Show help if no options provided
|
|
811
|
+
program.help()
|
|
812
|
+
}
|
|
813
|
+
} catch (error) {
|
|
814
|
+
// Handle any uncaught errors
|
|
815
|
+
console.error(chalk.red('\n✗ Unexpected error:'), error.message)
|
|
816
|
+
if (process.env.DEBUG) {
|
|
817
|
+
console.error(error.stack)
|
|
818
|
+
}
|
|
819
|
+
process.exit(1)
|
|
820
|
+
}
|
|
821
|
+
})()
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "skillscokac",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI tool to install and manage Claude Code skills from skills.cokac.com",
|
|
5
|
+
"main": "bin/skillscokac.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"skillscokac": "./bin/skillscokac.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
11
|
+
"prepublishOnly": "echo 'Running pre-publish checks...' && node bin/skillscokac.js --help"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"claude",
|
|
15
|
+
"claude-code",
|
|
16
|
+
"claude-ai",
|
|
17
|
+
"skills",
|
|
18
|
+
"cli",
|
|
19
|
+
"install",
|
|
20
|
+
"ai",
|
|
21
|
+
"assistant",
|
|
22
|
+
"marketplace",
|
|
23
|
+
"skill-manager",
|
|
24
|
+
"cokac"
|
|
25
|
+
],
|
|
26
|
+
"author": "코드깎는노인 <monogatree@gmail.com>",
|
|
27
|
+
"license": "ISC",
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=14.0.0"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/kstost/skillscokac.git"
|
|
34
|
+
},
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/kstost/skillscokac/issues"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://skills.cokac.com",
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"adm-zip": "^0.5.16",
|
|
41
|
+
"axios": "^1.6.0",
|
|
42
|
+
"boxen": "^5.1.2",
|
|
43
|
+
"chalk": "^4.1.2",
|
|
44
|
+
"commander": "^11.1.0",
|
|
45
|
+
"inquirer": "^8.2.6",
|
|
46
|
+
"ora": "^5.4.1",
|
|
47
|
+
"yaml": "^2.3.4"
|
|
48
|
+
},
|
|
49
|
+
"files": [
|
|
50
|
+
"bin/",
|
|
51
|
+
"README.md",
|
|
52
|
+
"LICENSE"
|
|
53
|
+
]
|
|
54
|
+
}
|