@biggora/claude-plugins 1.2.2 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/package.json +1 -1
- package/registry/registry.json +15 -0
- package/specs/coding.md +11 -0
- package/src/commands/skills/add.js +115 -31
- package/src/commands/skills/list.js +25 -52
- package/src/commands/skills/remove.js +45 -27
- package/src/commands/skills/resolve.js +104 -0
- package/src/commands/skills/update.js +58 -74
- package/src/config.js +5 -0
- package/src/skills/nest-best-practices/SKILL.md +251 -0
- package/src/skills/nest-best-practices/references/best-practices-request-lifecycle.md +158 -0
- package/src/skills/nest-best-practices/references/cli-monorepo.md +106 -0
- package/src/skills/nest-best-practices/references/cli-overview.md +157 -0
- package/src/skills/nest-best-practices/references/core-controllers.md +165 -0
- package/src/skills/nest-best-practices/references/core-dependency-injection.md +179 -0
- package/src/skills/nest-best-practices/references/core-middleware.md +139 -0
- package/src/skills/nest-best-practices/references/core-modules.md +138 -0
- package/src/skills/nest-best-practices/references/core-providers.md +188 -0
- package/src/skills/nest-best-practices/references/faq-raw-body-hybrid.md +122 -0
- package/src/skills/nest-best-practices/references/fundamentals-circular-dependency.md +89 -0
- package/src/skills/nest-best-practices/references/fundamentals-custom-decorators.md +107 -0
- package/src/skills/nest-best-practices/references/fundamentals-dynamic-modules.md +125 -0
- package/src/skills/nest-best-practices/references/fundamentals-exception-filters.md +202 -0
- package/src/skills/nest-best-practices/references/fundamentals-execution-context.md +107 -0
- package/src/skills/nest-best-practices/references/fundamentals-guards.md +136 -0
- package/src/skills/nest-best-practices/references/fundamentals-interceptors.md +187 -0
- package/src/skills/nest-best-practices/references/fundamentals-lazy-loading.md +89 -0
- package/src/skills/nest-best-practices/references/fundamentals-lifecycle-events.md +87 -0
- package/src/skills/nest-best-practices/references/fundamentals-module-reference.md +107 -0
- package/src/skills/nest-best-practices/references/fundamentals-pipes.md +197 -0
- package/src/skills/nest-best-practices/references/fundamentals-provider-scopes.md +92 -0
- package/src/skills/nest-best-practices/references/fundamentals-testing.md +142 -0
- package/src/skills/nest-best-practices/references/graphql-overview.md +233 -0
- package/src/skills/nest-best-practices/references/graphql-resolvers-mutations.md +199 -0
- package/src/skills/nest-best-practices/references/graphql-scalars-unions-enums.md +180 -0
- package/src/skills/nest-best-practices/references/graphql-subscriptions.md +228 -0
- package/src/skills/nest-best-practices/references/microservices-grpc.md +175 -0
- package/src/skills/nest-best-practices/references/microservices-overview.md +221 -0
- package/src/skills/nest-best-practices/references/microservices-transports.md +119 -0
- package/src/skills/nest-best-practices/references/openapi-swagger.md +207 -0
- package/src/skills/nest-best-practices/references/recipes-authentication.md +97 -0
- package/src/skills/nest-best-practices/references/recipes-cqrs.md +176 -0
- package/src/skills/nest-best-practices/references/recipes-crud-generator.md +87 -0
- package/src/skills/nest-best-practices/references/recipes-documentation.md +93 -0
- package/src/skills/nest-best-practices/references/recipes-mongoose.md +153 -0
- package/src/skills/nest-best-practices/references/recipes-prisma.md +98 -0
- package/src/skills/nest-best-practices/references/recipes-terminus.md +148 -0
- package/src/skills/nest-best-practices/references/recipes-typeorm.md +122 -0
- package/src/skills/nest-best-practices/references/security-authorization.md +196 -0
- package/src/skills/nest-best-practices/references/security-cors-helmet-rate-limiting.md +204 -0
- package/src/skills/nest-best-practices/references/security-encryption-hashing.md +93 -0
- package/src/skills/nest-best-practices/references/techniques-caching.md +142 -0
- package/src/skills/nest-best-practices/references/techniques-compression-streaming-sse.md +194 -0
- package/src/skills/nest-best-practices/references/techniques-configuration.md +132 -0
- package/src/skills/nest-best-practices/references/techniques-database.md +153 -0
- package/src/skills/nest-best-practices/references/techniques-events.md +163 -0
- package/src/skills/nest-best-practices/references/techniques-fastify.md +137 -0
- package/src/skills/nest-best-practices/references/techniques-file-upload.md +140 -0
- package/src/skills/nest-best-practices/references/techniques-http-module.md +176 -0
- package/src/skills/nest-best-practices/references/techniques-logging.md +146 -0
- package/src/skills/nest-best-practices/references/techniques-mvc-serve-static.md +132 -0
- package/src/skills/nest-best-practices/references/techniques-queues.md +162 -0
- package/src/skills/nest-best-practices/references/techniques-serialization.md +158 -0
- package/src/skills/nest-best-practices/references/techniques-sessions-cookies.md +167 -0
- package/src/skills/nest-best-practices/references/techniques-task-scheduling.md +166 -0
- package/src/skills/nest-best-practices/references/techniques-validation.md +126 -0
- package/src/skills/nest-best-practices/references/techniques-versioning.md +153 -0
- package/src/skills/nest-best-practices/references/websockets-advanced.md +96 -0
- package/src/skills/nest-best-practices/references/websockets-gateways.md +215 -0
- package/src/skills/typescript-expert/SKILL.md +145 -0
- package/src/skills/typescript-expert/commands/typescript-fix.md +65 -0
- package/src/skills/typescript-expert/references/advanced-conditional-types.md +190 -0
- package/src/skills/typescript-expert/references/advanced-decorators.md +243 -0
- package/src/skills/typescript-expert/references/advanced-mapped-types.md +223 -0
- package/src/skills/typescript-expert/references/advanced-template-literals.md +209 -0
- package/src/skills/typescript-expert/references/advanced-type-guards.md +308 -0
- package/src/skills/typescript-expert/references/best-practices-patterns.md +313 -0
- package/src/skills/typescript-expert/references/best-practices-performance.md +185 -0
- package/src/skills/typescript-expert/references/best-practices-tsconfig.md +242 -0
- package/src/skills/typescript-expert/references/core-generics.md +246 -0
- package/src/skills/typescript-expert/references/core-interfaces-types.md +231 -0
- package/src/skills/typescript-expert/references/core-type-system.md +261 -0
- package/src/skills/typescript-expert/references/core-utility-types.md +235 -0
- package/src/skills/typescript-expert/references/features-ts5x.md +370 -0
package/README.md
CHANGED
|
@@ -67,6 +67,7 @@ claude-plugins skills remove commafeed-api
|
|
|
67
67
|
| `vite-best-practices` | frontend | Vite 8 build tool best practices — configuration, Rolldown/Oxc migration, plugin API, SSR, library mode, and virtual modules |
|
|
68
68
|
| `google-merchant-api` | workflow | Work with Google Merchant Center APIs — Merchant API (v1) and Content API for Shopping (v2.1) for products, inventory, promotions, and reports |
|
|
69
69
|
| `lv-aggregators-api` | workflow | Generate and manage XML product feeds for Latvian price comparison platforms — Salidzini.lv and KurPirkt.lv integration |
|
|
70
|
+
| `typescript-expert` | code-quality | TypeScript type system, generics, utility types, advanced patterns, tsconfig, and TS 5.x features — includes `/typescript-fix` slash command |
|
|
70
71
|
| `youtube-thumbnail` | other | Generates professional YouTube thumbnails in 11 strategic styles with auto-detection of AI image backends and Pillow compositing |
|
|
71
72
|
|
|
72
73
|
#### Install Examples
|
|
@@ -90,6 +91,7 @@ npx skills add https://github.com/biggora/claude-plugins-registry --skill tailwi
|
|
|
90
91
|
npx skills add https://github.com/biggora/claude-plugins-registry --skill vite-best-practices
|
|
91
92
|
npx skills add https://github.com/biggora/claude-plugins-registry --skill google-merchant-api
|
|
92
93
|
npx skills add https://github.com/biggora/claude-plugins-registry --skill lv-aggregators-api
|
|
94
|
+
npx skills add https://github.com/biggora/claude-plugins-registry --skill typescript-expert
|
|
93
95
|
```
|
|
94
96
|
|
|
95
97
|
## Plugins
|
|
@@ -224,6 +226,7 @@ Skills are simpler than plugins — just a SKILL.md file with optional supportin
|
|
|
224
226
|
```
|
|
225
227
|
my-skill/
|
|
226
228
|
SKILL.md # Required: skill instructions with YAML frontmatter
|
|
229
|
+
commands/ # Slash commands (optional, copied to ~/.claude/commands/ on install)
|
|
227
230
|
references/ # Reference docs (optional)
|
|
228
231
|
scripts/ # Helper scripts (optional)
|
|
229
232
|
```
|
|
@@ -260,8 +263,9 @@ The plugin registry is a GitHub-hosted JSON file at [biggora/claude-plugins-regi
|
|
|
260
263
|
|
|
261
264
|
- **Plugins** are installed via `git clone` to `~/.claude/plugins/<name>`
|
|
262
265
|
- **Skills** are installed via `git clone` + copy to `~/.claude/skills/<name>`
|
|
266
|
+
- **Slash commands** bundled with skills are automatically copied to `~/.claude/commands/`
|
|
263
267
|
- Updates use `git pull --ff-only` to safely fast-forward
|
|
264
|
-
- Claude Code automatically discovers plugins
|
|
268
|
+
- Claude Code automatically discovers plugins in `~/.claude/plugins/`, skills in `~/.claude/skills/`, and commands in `~/.claude/commands/`
|
|
265
269
|
- Restart Claude Code after installing or removing plugins/skills
|
|
266
270
|
|
|
267
271
|
## Requirements
|
package/package.json
CHANGED
package/registry/registry.json
CHANGED
|
@@ -314,6 +314,21 @@
|
|
|
314
314
|
"commands": [],
|
|
315
315
|
"category": "workflow",
|
|
316
316
|
"type": "skill"
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
"name": "typescript-expert",
|
|
320
|
+
"version": "1.0.0",
|
|
321
|
+
"description": "TypeScript language expertise — type system, generics, utility types, advanced type patterns, tsconfig configuration, and TS 5.x features with /typescript-fix slash command for auto-resolving type errors",
|
|
322
|
+
"author": {
|
|
323
|
+
"name": "biggora",
|
|
324
|
+
"url": "https://github.com/biggora"
|
|
325
|
+
},
|
|
326
|
+
"repository": "https://github.com/biggora/claude-plugins-registry",
|
|
327
|
+
"keywords": ["typescript", "types", "generics", "tsconfig", "type-checking", "code-quality"],
|
|
328
|
+
"license": "MIT",
|
|
329
|
+
"commands": ["/typescript-fix"],
|
|
330
|
+
"category": "code-quality",
|
|
331
|
+
"type": "skill"
|
|
317
332
|
}
|
|
318
333
|
]
|
|
319
334
|
}
|
package/specs/coding.md
CHANGED
|
@@ -13,6 +13,7 @@ salidzini + kurpirkt
|
|
|
13
13
|
https://www.salidzini.lv/shops_info.php
|
|
14
14
|
https://www.kurpirkt.lv/veikaliem.php
|
|
15
15
|
|
|
16
|
+
nest--best-practices
|
|
16
17
|
|
|
17
18
|
add vite-best-practices, tailwindcss-best-practices skills to registry/registry.json and README.md
|
|
18
19
|
|
|
@@ -20,5 +21,15 @@ create skill google-merchant-api in src/skills/google-merchant-api. The skill sh
|
|
|
20
21
|
|
|
21
22
|
create skill lv-aggregators-api in src/skills/lv-aggregators-api. The skill should include knowledge of how to work with Aggregators APIs, how to integrate them into projects, formats and data types, etc. Aggregators Docs: https://www.salidzini.lv/shops_info.php, https://www.kurpirkt.lv/veikaliem.php
|
|
22
23
|
|
|
24
|
+
create skill typescript-expert src/skills/typescript-expert. The skill should include knowledge of TypeScript language features, best practices, type system, advanced types, generics, utility types, and how to use TypeScript effectively in projects.
|
|
23
25
|
|
|
26
|
+
create skill eslint-expert src/skills/eslint-expert. The skill should include knowledge of ESLint configuration, rules, plugins, and how to use ESLint effectively in projects for code quality and consistency.
|
|
24
27
|
|
|
28
|
+
create slash command typescript-fix for the TypeScript expert (typescript-expert) skill. with some content like this:
|
|
29
|
+
"You are a TypeScript expert with deep knowledge of complex typing patterns, modern JavaScript (ES6+), ESLint configuration, and enterprise-grade development practices. You specialize in building reliable, maintainable, and performant TypeScript solutions for complex business problems, with a solid understanding of JavaScript fundamentals and code quality tools. Read package.json to identify the command to run the linter. Use the `typescript-expert` skill to run `npm run typecheck`, `pnpm run typecheck`, or `tsc --noEmit` in the project directory and resolve TypeScript errors."
|
|
30
|
+
|
|
31
|
+
the slash command is copied to the same skill subfolder ~/.claude/skills/typescript-expert/commands/typescript-fix.md, but must be copied to ~/.claude/commands/typescript-fix.md
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
try this command npx skills add ./ --skill typescript-expert and check C:\Users\biggora\.claude\commands. Looks
|
|
35
|
+
like it does not work
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
|
-
import { existsSync, readFileSync, readdirSync, cpSync, rmSync, mkdirSync, writeFileSync, statSync } from 'node:fs';
|
|
3
|
-
import { join, basename } from 'node:path';
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, cpSync, rmSync, mkdirSync, writeFileSync, statSync, copyFileSync } from 'node:fs';
|
|
3
|
+
import { join, basename, resolve } from 'node:path';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
|
-
import { getSkillsDir } from '../../config.js';
|
|
5
|
+
import { getSkillsDir, getCommandsDir } from '../../config.js';
|
|
6
6
|
import { fetchRegistry, findPlugin } from '../../registry.js';
|
|
7
7
|
import { log, spinner } from '../../utils.js';
|
|
8
|
+
import { detectComponents, findInstalledSkill } from './resolve.js';
|
|
8
9
|
|
|
9
10
|
export function parseFrontmatter(content) {
|
|
10
11
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
@@ -48,6 +49,15 @@ function isUrl(str) {
|
|
|
48
49
|
return str.startsWith('http://') || str.startsWith('https://') || str.startsWith('git@');
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
function isLocalPath(str) {
|
|
53
|
+
if (str.startsWith('./') || str.startsWith('../') || str.startsWith('/')) return true;
|
|
54
|
+
// Windows absolute paths like C:\ or E:\
|
|
55
|
+
if (/^[a-zA-Z]:[/\\]/.test(str)) return true;
|
|
56
|
+
// Explicit . for current directory
|
|
57
|
+
if (str === '.') return true;
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
51
61
|
function makeTempDir() {
|
|
52
62
|
const dir = join(tmpdir(), `claude-skill-${Date.now()}`);
|
|
53
63
|
mkdirSync(dir, { recursive: true });
|
|
@@ -56,9 +66,19 @@ function makeTempDir() {
|
|
|
56
66
|
|
|
57
67
|
export async function add(source, options = {}) {
|
|
58
68
|
let repoUrl = source;
|
|
69
|
+
let sourceDir = null;
|
|
59
70
|
|
|
60
|
-
|
|
61
|
-
|
|
71
|
+
if (isLocalPath(source)) {
|
|
72
|
+
// Local directory — use directly, no git clone
|
|
73
|
+
sourceDir = resolve(source);
|
|
74
|
+
if (!existsSync(sourceDir)) {
|
|
75
|
+
log.error(`Local path "${sourceDir}" does not exist`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
repoUrl = sourceDir;
|
|
79
|
+
log.info(`Using local path: ${sourceDir}`);
|
|
80
|
+
} else if (!isUrl(source)) {
|
|
81
|
+
// Not a URL and not a local path — look up in registry
|
|
62
82
|
const registry = await fetchRegistry();
|
|
63
83
|
const entry = findPlugin(registry, source);
|
|
64
84
|
if (!entry) {
|
|
@@ -70,29 +90,36 @@ export async function add(source, options = {}) {
|
|
|
70
90
|
log.info(`Resolved "${source}" to ${repoUrl}`);
|
|
71
91
|
}
|
|
72
92
|
|
|
73
|
-
|
|
74
|
-
const spin = spinner(`Cloning ${repoUrl}...`);
|
|
75
|
-
spin.start();
|
|
93
|
+
let tmpDir = null;
|
|
76
94
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
95
|
+
// Clone if remote URL, otherwise use local path directly
|
|
96
|
+
if (!sourceDir) {
|
|
97
|
+
tmpDir = makeTempDir();
|
|
98
|
+
const spin = spinner(`Cloning ${repoUrl}...`);
|
|
99
|
+
spin.start();
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const gitUrl = repoUrl.endsWith('.git') ? repoUrl : `${repoUrl}.git`;
|
|
103
|
+
execFileSync('git', ['clone', '--depth', '1', gitUrl, tmpDir], {
|
|
104
|
+
stdio: 'pipe',
|
|
105
|
+
});
|
|
106
|
+
spin.succeed('Repository cloned');
|
|
107
|
+
} catch (err) {
|
|
108
|
+
spin.fail('Failed to clone repository');
|
|
109
|
+
log.error(err.message);
|
|
110
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
88
113
|
}
|
|
89
114
|
|
|
115
|
+
const searchRoot = sourceDir || tmpDir;
|
|
116
|
+
|
|
90
117
|
// Find all skill directories
|
|
91
|
-
const skillDirs = findSkillDirs(
|
|
118
|
+
const skillDirs = findSkillDirs(searchRoot);
|
|
92
119
|
|
|
93
120
|
if (!skillDirs.length) {
|
|
94
121
|
log.error('No SKILL.md found in repository');
|
|
95
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
122
|
+
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
|
|
96
123
|
process.exit(1);
|
|
97
124
|
}
|
|
98
125
|
|
|
@@ -117,22 +144,22 @@ export async function add(source, options = {}) {
|
|
|
117
144
|
const fm = parseFrontmatter(readFileSync(join(d, 'SKILL.md'), 'utf-8'));
|
|
118
145
|
log.dim(` - ${fm.name || basename(d)}`);
|
|
119
146
|
}
|
|
120
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
147
|
+
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
|
|
121
148
|
process.exit(1);
|
|
122
149
|
}
|
|
123
150
|
} else if (skillDirs.length === 1) {
|
|
124
151
|
targetDir = skillDirs[0];
|
|
125
152
|
} else {
|
|
126
153
|
// Check if root has SKILL.md
|
|
127
|
-
if (existsSync(join(
|
|
128
|
-
targetDir =
|
|
154
|
+
if (existsSync(join(searchRoot, 'SKILL.md'))) {
|
|
155
|
+
targetDir = searchRoot;
|
|
129
156
|
} else {
|
|
130
157
|
log.warn('Multiple skills found. Use --skill <name> to select one:');
|
|
131
158
|
for (const d of skillDirs) {
|
|
132
159
|
const fm = parseFrontmatter(readFileSync(join(d, 'SKILL.md'), 'utf-8'));
|
|
133
160
|
log.dim(` - ${fm.name || basename(d)}`);
|
|
134
161
|
}
|
|
135
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
162
|
+
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
|
|
136
163
|
process.exit(1);
|
|
137
164
|
}
|
|
138
165
|
}
|
|
@@ -140,19 +167,29 @@ export async function add(source, options = {}) {
|
|
|
140
167
|
// Parse skill metadata
|
|
141
168
|
const skillMd = readFileSync(join(targetDir, 'SKILL.md'), 'utf-8');
|
|
142
169
|
const frontmatter = parseFrontmatter(skillMd);
|
|
143
|
-
const skillName = frontmatter.name || options.skill || basename(targetDir ===
|
|
170
|
+
const skillName = frontmatter.name || options.skill || basename(targetDir === searchRoot ? repoUrl.replace(/\.git$/, '').split('/').pop() : targetDir);
|
|
171
|
+
|
|
172
|
+
// Detect components alongside SKILL.md
|
|
173
|
+
const components = detectComponents(targetDir);
|
|
144
174
|
|
|
145
175
|
const skillsDir = getSkillsDir();
|
|
146
176
|
const dest = join(skillsDir, skillName);
|
|
147
177
|
|
|
148
|
-
|
|
149
|
-
|
|
178
|
+
// Check both locations for existing installation
|
|
179
|
+
const existing = findInstalledSkill(skillName);
|
|
180
|
+
if (existing) {
|
|
181
|
+
log.warn(`Skill "${skillName}" is already installed at ${existing.dir}`);
|
|
150
182
|
log.dim(`Run "claude-plugins skills update ${skillName}" to update it`);
|
|
151
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
183
|
+
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (existsSync(dest)) {
|
|
188
|
+
log.warn(`"${skillName}" already exists at ${dest}`);
|
|
189
|
+
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
|
|
152
190
|
return;
|
|
153
191
|
}
|
|
154
192
|
|
|
155
|
-
// Copy skill files
|
|
156
193
|
const spin2 = spinner(`Installing skill "${skillName}"...`);
|
|
157
194
|
spin2.start();
|
|
158
195
|
|
|
@@ -165,6 +202,9 @@ export async function add(source, options = {}) {
|
|
|
165
202
|
rmSync(gitInDest, { recursive: true, force: true });
|
|
166
203
|
}
|
|
167
204
|
|
|
205
|
+
// Copy slash commands to ~/.claude/commands/ so Claude Code discovers them
|
|
206
|
+
const installedCommands = installCommands(dest, skillName);
|
|
207
|
+
|
|
168
208
|
// Write origin metadata
|
|
169
209
|
writeFileSync(
|
|
170
210
|
join(dest, '.origin.json'),
|
|
@@ -172,6 +212,8 @@ export async function add(source, options = {}) {
|
|
|
172
212
|
{
|
|
173
213
|
repository: repoUrl,
|
|
174
214
|
skill: options.skill || null,
|
|
215
|
+
components,
|
|
216
|
+
installedCommands,
|
|
175
217
|
installedAt: new Date().toISOString(),
|
|
176
218
|
},
|
|
177
219
|
null,
|
|
@@ -184,11 +226,53 @@ export async function add(source, options = {}) {
|
|
|
184
226
|
if (frontmatter.description) {
|
|
185
227
|
log.dim(` ${frontmatter.description}`);
|
|
186
228
|
}
|
|
229
|
+
|
|
230
|
+
if (installedCommands.length) {
|
|
231
|
+
log.info(`Commands: ${installedCommands.map((c) => `/${c}`).join(', ')}`);
|
|
232
|
+
log.dim(` Copied to ${getCommandsDir()}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (components.length > 1) {
|
|
236
|
+
log.info(`Detected: ${components.join(', ')}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
187
239
|
log.dim('\nRestart Claude Code to load the skill.');
|
|
188
240
|
} catch (err) {
|
|
189
241
|
spin2.fail(`Failed to install skill "${skillName}"`);
|
|
190
242
|
log.error(err.message);
|
|
191
243
|
} finally {
|
|
192
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
244
|
+
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Copy command .md files from skill's commands/ dir to ~/.claude/commands/.
|
|
250
|
+
* Returns array of installed command names (without extension).
|
|
251
|
+
*/
|
|
252
|
+
function installCommands(skillDir, skillName) {
|
|
253
|
+
const cmdsDir = join(skillDir, 'commands');
|
|
254
|
+
if (!existsSync(cmdsDir)) return [];
|
|
255
|
+
|
|
256
|
+
const commandsDir = getCommandsDir();
|
|
257
|
+
const installed = [];
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
const files = readdirSync(cmdsDir).filter((f) => f.endsWith('.md'));
|
|
261
|
+
for (const file of files) {
|
|
262
|
+
const src = join(cmdsDir, file);
|
|
263
|
+
const destFile = join(commandsDir, file);
|
|
264
|
+
|
|
265
|
+
if (existsSync(destFile)) {
|
|
266
|
+
log.warn(`Command "${file}" already exists in ${commandsDir}, skipping`);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
copyFileSync(src, destFile);
|
|
271
|
+
installed.push(file.replace(/\.md$/, ''));
|
|
272
|
+
}
|
|
273
|
+
} catch {
|
|
274
|
+
// ignore errors reading commands dir
|
|
193
275
|
}
|
|
276
|
+
|
|
277
|
+
return installed;
|
|
194
278
|
}
|
|
@@ -1,52 +1,25 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
let description = '';
|
|
27
|
-
let repo = '-';
|
|
28
|
-
|
|
29
|
-
if (existsSync(skillMdPath)) {
|
|
30
|
-
try {
|
|
31
|
-
const fm = parseFrontmatter(readFileSync(skillMdPath, 'utf-8'));
|
|
32
|
-
description = fm.description || '';
|
|
33
|
-
} catch {
|
|
34
|
-
// ignore
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (existsSync(originPath)) {
|
|
39
|
-
try {
|
|
40
|
-
const origin = JSON.parse(readFileSync(originPath, 'utf-8'));
|
|
41
|
-
repo = origin.repository || '-';
|
|
42
|
-
} catch {
|
|
43
|
-
// ignore
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return [entry.name, truncate(description, 50), truncate(repo, 40)];
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
formatTable(rows, ['Name', 'Description', 'Repository']);
|
|
51
|
-
console.log();
|
|
52
|
-
}
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { log, formatTable, truncate } from '../../utils.js';
|
|
3
|
+
import { listAllSkills } from './resolve.js';
|
|
4
|
+
|
|
5
|
+
export async function list() {
|
|
6
|
+
const skills = listAllSkills();
|
|
7
|
+
|
|
8
|
+
if (!skills.length) {
|
|
9
|
+
log.info('No skills installed');
|
|
10
|
+
log.dim('Run "claude-plugins skills add <source>" to install a skill');
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
console.log(chalk.bold(`\n ${skills.length} skill${skills.length === 1 ? '' : 's'} installed\n`));
|
|
15
|
+
|
|
16
|
+
const rows = skills.map(({ name, meta }) => {
|
|
17
|
+
const cmds = meta.commands.length
|
|
18
|
+
? meta.commands.map((c) => `/${c}`).join(', ')
|
|
19
|
+
: '';
|
|
20
|
+
return [name, truncate(meta.description, 45), cmds, truncate(meta.repository, 35)];
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
formatTable(rows, ['Name', 'Description', 'Commands', 'Repository']);
|
|
24
|
+
console.log();
|
|
25
|
+
}
|
|
@@ -1,27 +1,45 @@
|
|
|
1
|
-
import { existsSync, rmSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import {
|
|
4
|
-
import { log, spinner } from '../../utils.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
log.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
spin
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
1
|
+
import { existsSync, rmSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { getCommandsDir } from '../../config.js';
|
|
4
|
+
import { log, spinner } from '../../utils.js';
|
|
5
|
+
import { findInstalledSkill, readOrigin } from './resolve.js';
|
|
6
|
+
|
|
7
|
+
export async function remove(name) {
|
|
8
|
+
const found = findInstalledSkill(name);
|
|
9
|
+
|
|
10
|
+
if (!found) {
|
|
11
|
+
log.error(`Skill "${name}" is not installed`);
|
|
12
|
+
log.dim('Run "claude-plugins skills list" to see installed skills');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const spin = spinner(`Removing skill "${name}"...`);
|
|
17
|
+
spin.start();
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
// Clean up commands that were copied to ~/.claude/commands/
|
|
21
|
+
const origin = readOrigin(found.dir);
|
|
22
|
+
if (origin && origin.installedCommands?.length) {
|
|
23
|
+
const commandsDir = getCommandsDir();
|
|
24
|
+
for (const cmd of origin.installedCommands) {
|
|
25
|
+
const cmdFile = join(commandsDir, `${cmd}.md`);
|
|
26
|
+
if (existsSync(cmdFile)) {
|
|
27
|
+
rmSync(cmdFile);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
rmSync(found.dir, { recursive: true, force: true });
|
|
33
|
+
spin.succeed(`Removed skill "${name}"`);
|
|
34
|
+
|
|
35
|
+
if (origin?.installedCommands?.length) {
|
|
36
|
+
log.dim(` Also removed commands: ${origin.installedCommands.map((c) => `/${c}`).join(', ')}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
log.dim('Restart Claude Code to apply changes.');
|
|
40
|
+
} catch (err) {
|
|
41
|
+
spin.fail(`Failed to remove skill "${name}"`);
|
|
42
|
+
log.error(err.message);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { getSkillsDir } from '../../config.js';
|
|
4
|
+
import { parseFrontmatter } from './add.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Component directories that can accompany a SKILL.md.
|
|
8
|
+
*/
|
|
9
|
+
export const COMPONENT_DIRS = ['commands', 'hooks', 'agents'];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Detect which components exist in a directory.
|
|
13
|
+
* Returns an array of found component names.
|
|
14
|
+
*/
|
|
15
|
+
export function detectComponents(dir) {
|
|
16
|
+
const found = [];
|
|
17
|
+
if (existsSync(join(dir, 'SKILL.md'))) found.push('skill');
|
|
18
|
+
for (const name of COMPONENT_DIRS) {
|
|
19
|
+
if (existsSync(join(dir, name))) found.push(name);
|
|
20
|
+
}
|
|
21
|
+
return found;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Read .origin.json from a skill directory.
|
|
26
|
+
*/
|
|
27
|
+
export function readOrigin(dir) {
|
|
28
|
+
const originPath = join(dir, '.origin.json');
|
|
29
|
+
if (!existsSync(originPath)) return null;
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(readFileSync(originPath, 'utf-8'));
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Read skill metadata from a directory (SKILL.md frontmatter + .origin.json).
|
|
39
|
+
*/
|
|
40
|
+
export function readSkillMeta(dir, dirName) {
|
|
41
|
+
const skillMdPath = join(dir, 'SKILL.md');
|
|
42
|
+
const meta = { name: dirName, description: '', repository: '-', commands: [] };
|
|
43
|
+
|
|
44
|
+
if (existsSync(skillMdPath)) {
|
|
45
|
+
try {
|
|
46
|
+
const fm = parseFrontmatter(readFileSync(skillMdPath, 'utf-8'));
|
|
47
|
+
meta.name = fm.name || dirName;
|
|
48
|
+
meta.description = fm.description || '';
|
|
49
|
+
} catch {
|
|
50
|
+
// ignore
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const origin = readOrigin(dir);
|
|
55
|
+
if (origin) {
|
|
56
|
+
meta.repository = origin.repository || '-';
|
|
57
|
+
meta.skill = origin.skill || null;
|
|
58
|
+
meta.commands = origin.installedCommands || [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return meta;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Find a skill by name in ~/.claude/skills/.
|
|
66
|
+
* Returns { dir, location } or null.
|
|
67
|
+
*/
|
|
68
|
+
export function findInstalledSkill(name) {
|
|
69
|
+
const skillsDir = getSkillsDir();
|
|
70
|
+
const skillPath = join(skillsDir, name);
|
|
71
|
+
|
|
72
|
+
if (existsSync(skillPath) && existsSync(join(skillPath, 'SKILL.md'))) {
|
|
73
|
+
return { dir: skillPath, location: 'skills' };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* List all installed skills from ~/.claude/skills/.
|
|
81
|
+
* Returns array of { name, dir, meta }.
|
|
82
|
+
*/
|
|
83
|
+
export function listAllSkills() {
|
|
84
|
+
const skillsDir = getSkillsDir();
|
|
85
|
+
const results = [];
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const entries = readdirSync(skillsDir, { withFileTypes: true });
|
|
89
|
+
for (const entry of entries) {
|
|
90
|
+
if (!entry.isDirectory()) continue;
|
|
91
|
+
const dir = join(skillsDir, entry.name);
|
|
92
|
+
if (!existsSync(join(dir, 'SKILL.md'))) continue;
|
|
93
|
+
results.push({
|
|
94
|
+
name: entry.name,
|
|
95
|
+
dir,
|
|
96
|
+
meta: readSkillMeta(dir, entry.name),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
// directory might not exist yet
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return results;
|
|
104
|
+
}
|