@biggora/claude-plugins 1.3.0 → 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 +3 -1
- package/package.json +1 -1
- package/specs/coding.md +5 -0
- package/src/commands/skills/add.js +94 -66
- package/src/commands/skills/list.js +6 -4
- package/src/commands/skills/remove.js +22 -3
- package/src/commands/skills/resolve.js +12 -63
- package/src/config.js +5 -0
package/README.md
CHANGED
|
@@ -226,6 +226,7 @@ Skills are simpler than plugins — just a SKILL.md file with optional supportin
|
|
|
226
226
|
```
|
|
227
227
|
my-skill/
|
|
228
228
|
SKILL.md # Required: skill instructions with YAML frontmatter
|
|
229
|
+
commands/ # Slash commands (optional, copied to ~/.claude/commands/ on install)
|
|
229
230
|
references/ # Reference docs (optional)
|
|
230
231
|
scripts/ # Helper scripts (optional)
|
|
231
232
|
```
|
|
@@ -262,8 +263,9 @@ The plugin registry is a GitHub-hosted JSON file at [biggora/claude-plugins-regi
|
|
|
262
263
|
|
|
263
264
|
- **Plugins** are installed via `git clone` to `~/.claude/plugins/<name>`
|
|
264
265
|
- **Skills** are installed via `git clone` + copy to `~/.claude/skills/<name>`
|
|
266
|
+
- **Slash commands** bundled with skills are automatically copied to `~/.claude/commands/`
|
|
265
267
|
- Updates use `git pull --ff-only` to safely fast-forward
|
|
266
|
-
- Claude Code automatically discovers plugins
|
|
268
|
+
- Claude Code automatically discovers plugins in `~/.claude/plugins/`, skills in `~/.claude/skills/`, and commands in `~/.claude/commands/`
|
|
267
269
|
- Restart Claude Code after installing or removing plugins/skills
|
|
268
270
|
|
|
269
271
|
## Requirements
|
package/package.json
CHANGED
package/specs/coding.md
CHANGED
|
@@ -28,3 +28,8 @@ create skill eslint-expert src/skills/eslint-expert. The skill should include kn
|
|
|
28
28
|
create slash command typescript-fix for the TypeScript expert (typescript-expert) skill. with some content like this:
|
|
29
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
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,11 +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,
|
|
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 {
|
|
8
|
+
import { detectComponents, findInstalledSkill } from './resolve.js';
|
|
9
9
|
|
|
10
10
|
export function parseFrontmatter(content) {
|
|
11
11
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
@@ -49,6 +49,15 @@ function isUrl(str) {
|
|
|
49
49
|
return str.startsWith('http://') || str.startsWith('https://') || str.startsWith('git@');
|
|
50
50
|
}
|
|
51
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
|
+
|
|
52
61
|
function makeTempDir() {
|
|
53
62
|
const dir = join(tmpdir(), `claude-skill-${Date.now()}`);
|
|
54
63
|
mkdirSync(dir, { recursive: true });
|
|
@@ -57,9 +66,19 @@ function makeTempDir() {
|
|
|
57
66
|
|
|
58
67
|
export async function add(source, options = {}) {
|
|
59
68
|
let repoUrl = source;
|
|
69
|
+
let sourceDir = null;
|
|
60
70
|
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
63
82
|
const registry = await fetchRegistry();
|
|
64
83
|
const entry = findPlugin(registry, source);
|
|
65
84
|
if (!entry) {
|
|
@@ -71,29 +90,36 @@ export async function add(source, options = {}) {
|
|
|
71
90
|
log.info(`Resolved "${source}" to ${repoUrl}`);
|
|
72
91
|
}
|
|
73
92
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
93
|
+
let tmpDir = null;
|
|
94
|
+
|
|
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
|
+
}
|
|
89
113
|
}
|
|
90
114
|
|
|
115
|
+
const searchRoot = sourceDir || tmpDir;
|
|
116
|
+
|
|
91
117
|
// Find all skill directories
|
|
92
|
-
const skillDirs = findSkillDirs(
|
|
118
|
+
const skillDirs = findSkillDirs(searchRoot);
|
|
93
119
|
|
|
94
120
|
if (!skillDirs.length) {
|
|
95
121
|
log.error('No SKILL.md found in repository');
|
|
96
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
122
|
+
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
|
|
97
123
|
process.exit(1);
|
|
98
124
|
}
|
|
99
125
|
|
|
@@ -118,22 +144,22 @@ export async function add(source, options = {}) {
|
|
|
118
144
|
const fm = parseFrontmatter(readFileSync(join(d, 'SKILL.md'), 'utf-8'));
|
|
119
145
|
log.dim(` - ${fm.name || basename(d)}`);
|
|
120
146
|
}
|
|
121
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
147
|
+
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
|
|
122
148
|
process.exit(1);
|
|
123
149
|
}
|
|
124
150
|
} else if (skillDirs.length === 1) {
|
|
125
151
|
targetDir = skillDirs[0];
|
|
126
152
|
} else {
|
|
127
153
|
// Check if root has SKILL.md
|
|
128
|
-
if (existsSync(join(
|
|
129
|
-
targetDir =
|
|
154
|
+
if (existsSync(join(searchRoot, 'SKILL.md'))) {
|
|
155
|
+
targetDir = searchRoot;
|
|
130
156
|
} else {
|
|
131
157
|
log.warn('Multiple skills found. Use --skill <name> to select one:');
|
|
132
158
|
for (const d of skillDirs) {
|
|
133
159
|
const fm = parseFrontmatter(readFileSync(join(d, 'SKILL.md'), 'utf-8'));
|
|
134
160
|
log.dim(` - ${fm.name || basename(d)}`);
|
|
135
161
|
}
|
|
136
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
162
|
+
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
|
|
137
163
|
process.exit(1);
|
|
138
164
|
}
|
|
139
165
|
}
|
|
@@ -141,32 +167,30 @@ export async function add(source, options = {}) {
|
|
|
141
167
|
// Parse skill metadata
|
|
142
168
|
const skillMd = readFileSync(join(targetDir, 'SKILL.md'), 'utf-8');
|
|
143
169
|
const frontmatter = parseFrontmatter(skillMd);
|
|
144
|
-
const skillName = frontmatter.name || options.skill || basename(targetDir ===
|
|
170
|
+
const skillName = frontmatter.name || options.skill || basename(targetDir === searchRoot ? repoUrl.replace(/\.git$/, '').split('/').pop() : targetDir);
|
|
145
171
|
|
|
146
|
-
// Detect
|
|
172
|
+
// Detect components alongside SKILL.md
|
|
147
173
|
const components = detectComponents(targetDir);
|
|
148
|
-
const installAsPlugin = hasPluginComponents(targetDir);
|
|
149
174
|
|
|
150
|
-
const
|
|
151
|
-
const dest = join(
|
|
175
|
+
const skillsDir = getSkillsDir();
|
|
176
|
+
const dest = join(skillsDir, skillName);
|
|
152
177
|
|
|
153
178
|
// Check both locations for existing installation
|
|
154
179
|
const existing = findInstalledSkill(skillName);
|
|
155
180
|
if (existing) {
|
|
156
181
|
log.warn(`Skill "${skillName}" is already installed at ${existing.dir}`);
|
|
157
182
|
log.dim(`Run "claude-plugins skills update ${skillName}" to update it`);
|
|
158
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
183
|
+
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
|
|
159
184
|
return;
|
|
160
185
|
}
|
|
161
186
|
|
|
162
187
|
if (existsSync(dest)) {
|
|
163
188
|
log.warn(`"${skillName}" already exists at ${dest}`);
|
|
164
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
189
|
+
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
|
|
165
190
|
return;
|
|
166
191
|
}
|
|
167
192
|
|
|
168
|
-
const
|
|
169
|
-
const spin2 = spinner(`Installing ${label} "${skillName}"...`);
|
|
193
|
+
const spin2 = spinner(`Installing skill "${skillName}"...`);
|
|
170
194
|
spin2.start();
|
|
171
195
|
|
|
172
196
|
try {
|
|
@@ -178,24 +202,8 @@ export async function add(source, options = {}) {
|
|
|
178
202
|
rmSync(gitInDest, { recursive: true, force: true });
|
|
179
203
|
}
|
|
180
204
|
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
const pluginDir = join(dest, '.claude-plugin');
|
|
184
|
-
mkdirSync(pluginDir, { recursive: true });
|
|
185
|
-
|
|
186
|
-
const pluginJson = {
|
|
187
|
-
name: skillName,
|
|
188
|
-
version: '1.0.0',
|
|
189
|
-
description: frontmatter.description || `Skill: ${skillName}`,
|
|
190
|
-
commands: listCommandFiles(dest),
|
|
191
|
-
_generatedBy: 'claude-plugins skills add',
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
writeFileSync(
|
|
195
|
-
join(pluginDir, 'plugin.json'),
|
|
196
|
-
JSON.stringify(pluginJson, null, 2)
|
|
197
|
-
);
|
|
198
|
-
}
|
|
205
|
+
// Copy slash commands to ~/.claude/commands/ so Claude Code discovers them
|
|
206
|
+
const installedCommands = installCommands(dest, skillName);
|
|
199
207
|
|
|
200
208
|
// Write origin metadata
|
|
201
209
|
writeFileSync(
|
|
@@ -204,8 +212,8 @@ export async function add(source, options = {}) {
|
|
|
204
212
|
{
|
|
205
213
|
repository: repoUrl,
|
|
206
214
|
skill: options.skill || null,
|
|
207
|
-
installedAs: installAsPlugin ? 'plugin-skill' : 'skill',
|
|
208
215
|
components,
|
|
216
|
+
installedCommands,
|
|
209
217
|
installedAt: new Date().toISOString(),
|
|
210
218
|
},
|
|
211
219
|
null,
|
|
@@ -219,10 +227,13 @@ export async function add(source, options = {}) {
|
|
|
219
227
|
log.dim(` ${frontmatter.description}`);
|
|
220
228
|
}
|
|
221
229
|
|
|
222
|
-
if (
|
|
223
|
-
|
|
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) {
|
|
224
236
|
log.info(`Detected: ${components.join(', ')}`);
|
|
225
|
-
log.dim(`Installed as plugin so Claude Code discovers ${extras.join(', ')}`);
|
|
226
237
|
}
|
|
227
238
|
|
|
228
239
|
log.dim('\nRestart Claude Code to load the skill.');
|
|
@@ -230,21 +241,38 @@ export async function add(source, options = {}) {
|
|
|
230
241
|
spin2.fail(`Failed to install skill "${skillName}"`);
|
|
231
242
|
log.error(err.message);
|
|
232
243
|
} finally {
|
|
233
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
244
|
+
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
|
|
234
245
|
}
|
|
235
246
|
}
|
|
236
247
|
|
|
237
248
|
/**
|
|
238
|
-
*
|
|
249
|
+
* Copy command .md files from skill's commands/ dir to ~/.claude/commands/.
|
|
250
|
+
* Returns array of installed command names (without extension).
|
|
239
251
|
*/
|
|
240
|
-
function
|
|
241
|
-
const cmdsDir = join(
|
|
252
|
+
function installCommands(skillDir, skillName) {
|
|
253
|
+
const cmdsDir = join(skillDir, 'commands');
|
|
242
254
|
if (!existsSync(cmdsDir)) return [];
|
|
255
|
+
|
|
256
|
+
const commandsDir = getCommandsDir();
|
|
257
|
+
const installed = [];
|
|
258
|
+
|
|
243
259
|
try {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
+
}
|
|
247
273
|
} catch {
|
|
248
|
-
|
|
274
|
+
// ignore errors reading commands dir
|
|
249
275
|
}
|
|
276
|
+
|
|
277
|
+
return installed;
|
|
250
278
|
}
|
|
@@ -13,11 +13,13 @@ export async function list() {
|
|
|
13
13
|
|
|
14
14
|
console.log(chalk.bold(`\n ${skills.length} skill${skills.length === 1 ? '' : 's'} installed\n`));
|
|
15
15
|
|
|
16
|
-
const rows = skills.map(({ name, meta
|
|
17
|
-
const
|
|
18
|
-
|
|
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)];
|
|
19
21
|
});
|
|
20
22
|
|
|
21
|
-
formatTable(rows, ['Name', 'Description', '
|
|
23
|
+
formatTable(rows, ['Name', 'Description', 'Commands', 'Repository']);
|
|
22
24
|
console.log();
|
|
23
25
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { rmSync } from 'node:fs';
|
|
1
|
+
import { existsSync, rmSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { getCommandsDir } from '../../config.js';
|
|
2
4
|
import { log, spinner } from '../../utils.js';
|
|
3
|
-
import { findInstalledSkill } from './resolve.js';
|
|
5
|
+
import { findInstalledSkill, readOrigin } from './resolve.js';
|
|
4
6
|
|
|
5
7
|
export async function remove(name) {
|
|
6
8
|
const found = findInstalledSkill(name);
|
|
@@ -15,8 +17,25 @@ export async function remove(name) {
|
|
|
15
17
|
spin.start();
|
|
16
18
|
|
|
17
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
|
+
|
|
18
32
|
rmSync(found.dir, { recursive: true, force: true });
|
|
19
|
-
spin.succeed(`Removed skill "${name}"
|
|
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
|
+
|
|
20
39
|
log.dim('Restart Claude Code to apply changes.');
|
|
21
40
|
} catch (err) {
|
|
22
41
|
spin.fail(`Failed to remove skill "${name}"`);
|
|
@@ -1,41 +1,28 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { getSkillsDir
|
|
3
|
+
import { getSkillsDir } from '../../config.js';
|
|
4
4
|
import { parseFrontmatter } from './add.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
* When a skill repo contains any of these, it must be installed
|
|
9
|
-
* as a plugin so Claude Code can find them.
|
|
7
|
+
* Component directories that can accompany a SKILL.md.
|
|
10
8
|
*/
|
|
11
|
-
export const
|
|
9
|
+
export const COMPONENT_DIRS = ['commands', 'hooks', 'agents'];
|
|
12
10
|
|
|
13
11
|
/**
|
|
14
|
-
*
|
|
15
|
-
* beyond just SKILL.md + references.
|
|
16
|
-
*/
|
|
17
|
-
export function hasPluginComponents(dir) {
|
|
18
|
-
return PLUGIN_COMPONENT_DIRS.some((name) => {
|
|
19
|
-
const p = join(dir, name);
|
|
20
|
-
return existsSync(p);
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Detect which plugin components exist in a directory.
|
|
12
|
+
* Detect which components exist in a directory.
|
|
26
13
|
* Returns an array of found component names.
|
|
27
14
|
*/
|
|
28
15
|
export function detectComponents(dir) {
|
|
29
16
|
const found = [];
|
|
30
17
|
if (existsSync(join(dir, 'SKILL.md'))) found.push('skill');
|
|
31
|
-
for (const name of
|
|
18
|
+
for (const name of COMPONENT_DIRS) {
|
|
32
19
|
if (existsSync(join(dir, name))) found.push(name);
|
|
33
20
|
}
|
|
34
21
|
return found;
|
|
35
22
|
}
|
|
36
23
|
|
|
37
24
|
/**
|
|
38
|
-
* Read .origin.json from a skill
|
|
25
|
+
* Read .origin.json from a skill directory.
|
|
39
26
|
*/
|
|
40
27
|
export function readOrigin(dir) {
|
|
41
28
|
const originPath = join(dir, '.origin.json');
|
|
@@ -52,7 +39,7 @@ export function readOrigin(dir) {
|
|
|
52
39
|
*/
|
|
53
40
|
export function readSkillMeta(dir, dirName) {
|
|
54
41
|
const skillMdPath = join(dir, 'SKILL.md');
|
|
55
|
-
const meta = { name: dirName, description: '', repository: '-' };
|
|
42
|
+
const meta = { name: dirName, description: '', repository: '-', commands: [] };
|
|
56
43
|
|
|
57
44
|
if (existsSync(skillMdPath)) {
|
|
58
45
|
try {
|
|
@@ -67,53 +54,36 @@ export function readSkillMeta(dir, dirName) {
|
|
|
67
54
|
const origin = readOrigin(dir);
|
|
68
55
|
if (origin) {
|
|
69
56
|
meta.repository = origin.repository || '-';
|
|
70
|
-
meta.installedAs = origin.installedAs || 'skill';
|
|
71
57
|
meta.skill = origin.skill || null;
|
|
58
|
+
meta.commands = origin.installedCommands || [];
|
|
72
59
|
}
|
|
73
60
|
|
|
74
61
|
return meta;
|
|
75
62
|
}
|
|
76
63
|
|
|
77
64
|
/**
|
|
78
|
-
* Find a skill by name
|
|
65
|
+
* Find a skill by name in ~/.claude/skills/.
|
|
79
66
|
* Returns { dir, location } or null.
|
|
80
67
|
*/
|
|
81
68
|
export function findInstalledSkill(name) {
|
|
82
69
|
const skillsDir = getSkillsDir();
|
|
83
|
-
const pluginsDir = getPluginsDir();
|
|
84
|
-
|
|
85
|
-
// Check ~/.claude/skills/ first
|
|
86
70
|
const skillPath = join(skillsDir, name);
|
|
71
|
+
|
|
87
72
|
if (existsSync(skillPath) && existsSync(join(skillPath, 'SKILL.md'))) {
|
|
88
73
|
return { dir: skillPath, location: 'skills' };
|
|
89
74
|
}
|
|
90
75
|
|
|
91
|
-
// Check ~/.claude/plugins/ for skills installed as plugins
|
|
92
|
-
const pluginPath = join(pluginsDir, name);
|
|
93
|
-
if (existsSync(pluginPath)) {
|
|
94
|
-
const origin = readOrigin(pluginPath);
|
|
95
|
-
if (origin && origin.installedAs === 'plugin-skill') {
|
|
96
|
-
return { dir: pluginPath, location: 'plugins' };
|
|
97
|
-
}
|
|
98
|
-
// Also check if it has a SKILL.md (might be installed via skills add)
|
|
99
|
-
if (existsSync(join(pluginPath, 'SKILL.md'))) {
|
|
100
|
-
return { dir: pluginPath, location: 'plugins' };
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
76
|
return null;
|
|
105
77
|
}
|
|
106
78
|
|
|
107
79
|
/**
|
|
108
|
-
* List all installed skills from
|
|
109
|
-
* Returns array of { name, dir,
|
|
80
|
+
* List all installed skills from ~/.claude/skills/.
|
|
81
|
+
* Returns array of { name, dir, meta }.
|
|
110
82
|
*/
|
|
111
83
|
export function listAllSkills() {
|
|
112
84
|
const skillsDir = getSkillsDir();
|
|
113
|
-
const pluginsDir = getPluginsDir();
|
|
114
85
|
const results = [];
|
|
115
86
|
|
|
116
|
-
// Skills from ~/.claude/skills/
|
|
117
87
|
try {
|
|
118
88
|
const entries = readdirSync(skillsDir, { withFileTypes: true });
|
|
119
89
|
for (const entry of entries) {
|
|
@@ -123,7 +93,6 @@ export function listAllSkills() {
|
|
|
123
93
|
results.push({
|
|
124
94
|
name: entry.name,
|
|
125
95
|
dir,
|
|
126
|
-
location: 'skills',
|
|
127
96
|
meta: readSkillMeta(dir, entry.name),
|
|
128
97
|
});
|
|
129
98
|
}
|
|
@@ -131,25 +100,5 @@ export function listAllSkills() {
|
|
|
131
100
|
// directory might not exist yet
|
|
132
101
|
}
|
|
133
102
|
|
|
134
|
-
// Skills from ~/.claude/plugins/ (installed as plugin-skill)
|
|
135
|
-
try {
|
|
136
|
-
const entries = readdirSync(pluginsDir, { withFileTypes: true });
|
|
137
|
-
for (const entry of entries) {
|
|
138
|
-
if (!entry.isDirectory()) continue;
|
|
139
|
-
const dir = join(pluginsDir, entry.name);
|
|
140
|
-
const origin = readOrigin(dir);
|
|
141
|
-
if (origin && origin.installedAs === 'plugin-skill') {
|
|
142
|
-
results.push({
|
|
143
|
-
name: entry.name,
|
|
144
|
-
dir,
|
|
145
|
-
location: 'plugins',
|
|
146
|
-
meta: readSkillMeta(dir, entry.name),
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
} catch {
|
|
151
|
-
// directory might not exist yet
|
|
152
|
-
}
|
|
153
|
-
|
|
154
103
|
return results;
|
|
155
104
|
}
|
package/src/config.js
CHANGED
|
@@ -7,6 +7,7 @@ const isWindows = process.platform === 'win32';
|
|
|
7
7
|
|
|
8
8
|
export const PLUGINS_DIR = join(home, '.claude', 'plugins');
|
|
9
9
|
export const SKILLS_DIR = join(home, '.claude', 'skills');
|
|
10
|
+
export const COMMANDS_DIR = join(home, '.claude', 'commands');
|
|
10
11
|
export const CACHE_DIR = join(home, '.claude', '.cache', 'claude-plugins');
|
|
11
12
|
export const CACHE_TTL = 1000 * 60 * 15; // 15 minutes
|
|
12
13
|
export const REGISTRY_URL =
|
|
@@ -27,6 +28,10 @@ export function getSkillsDir() {
|
|
|
27
28
|
return ensureDir(SKILLS_DIR);
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
export function getCommandsDir() {
|
|
32
|
+
return ensureDir(COMMANDS_DIR);
|
|
33
|
+
}
|
|
34
|
+
|
|
30
35
|
export function getCacheDir() {
|
|
31
36
|
return ensureDir(CACHE_DIR);
|
|
32
37
|
}
|