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 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
+ }