@biggora/claude-plugins 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 +21 -0
- package/README.md +113 -0
- package/bin/cli.js +79 -0
- package/package.json +40 -0
- package/registry/registry.json +49 -0
- package/registry/schema.json +49 -0
- package/src/commands/info.js +45 -0
- package/src/commands/install.js +48 -0
- package/src/commands/list.js +42 -0
- package/src/commands/publish.js +124 -0
- package/src/commands/search.js +33 -0
- package/src/commands/uninstall.js +27 -0
- package/src/commands/update.js +72 -0
- package/src/config.js +27 -0
- package/src/registry.js +67 -0
- package/src/utils.js +34 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 biggora
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# claude-plugins
|
|
2
|
+
|
|
3
|
+
CLI marketplace for discovering, installing, and managing Claude Code plugins.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g claude-plugins
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or use directly with npx:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx claude-plugins search
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Commands
|
|
18
|
+
|
|
19
|
+
### Browse & Search
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# List all available plugins
|
|
23
|
+
claude-plugins search
|
|
24
|
+
|
|
25
|
+
# Search by name or keyword
|
|
26
|
+
claude-plugins search typescript
|
|
27
|
+
claude-plugins search code-quality
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Install & Remove
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Install a plugin
|
|
34
|
+
claude-plugins install code-optimizer
|
|
35
|
+
|
|
36
|
+
# Remove a plugin
|
|
37
|
+
claude-plugins uninstall code-optimizer
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Manage
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# List installed plugins
|
|
44
|
+
claude-plugins list
|
|
45
|
+
|
|
46
|
+
# Show plugin details
|
|
47
|
+
claude-plugins info code-optimizer
|
|
48
|
+
|
|
49
|
+
# Update all plugins
|
|
50
|
+
claude-plugins update
|
|
51
|
+
|
|
52
|
+
# Update a specific plugin
|
|
53
|
+
claude-plugins update code-optimizer
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Publish
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# From your plugin directory
|
|
60
|
+
cd my-plugin
|
|
61
|
+
claude-plugins publish
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
This validates your plugin and generates a registry entry. Submit a PR to [claude-plugins-registry](https://github.com/biggora/claude-plugins-registry) to list it.
|
|
65
|
+
|
|
66
|
+
## Plugin Structure
|
|
67
|
+
|
|
68
|
+
Plugins must have this minimum structure:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
my-plugin/
|
|
72
|
+
.claude-plugin/
|
|
73
|
+
plugin.json # Required: name, version, description
|
|
74
|
+
README.md # Required
|
|
75
|
+
commands/ # Slash commands
|
|
76
|
+
agents/ # Agent definitions
|
|
77
|
+
skills/ # Skill files
|
|
78
|
+
hooks/ # Hook definitions
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### plugin.json
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"name": "my-plugin",
|
|
86
|
+
"version": "1.0.0",
|
|
87
|
+
"description": "What it does",
|
|
88
|
+
"author": { "name": "you", "url": "https://github.com/you" },
|
|
89
|
+
"repository": "https://github.com/you/my-plugin",
|
|
90
|
+
"keywords": ["keyword1", "keyword2"],
|
|
91
|
+
"license": "MIT"
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Registry
|
|
96
|
+
|
|
97
|
+
The plugin registry is hosted at [github.com/biggora/claude-plugins-registry](https://github.com/biggora/claude-plugins-registry). The CLI fetches it on demand and caches for 15 minutes.
|
|
98
|
+
|
|
99
|
+
To add your plugin to the registry:
|
|
100
|
+
|
|
101
|
+
1. Ensure your plugin is on GitHub
|
|
102
|
+
2. Run `claude-plugins publish` to validate and generate a registry entry
|
|
103
|
+
3. Submit a PR adding the entry to `registry.json`
|
|
104
|
+
|
|
105
|
+
## How It Works
|
|
106
|
+
|
|
107
|
+
- Plugins are installed to `~/.claude/plugins/<name>` via `git clone`
|
|
108
|
+
- Updates use `git pull --ff-only`
|
|
109
|
+
- Claude Code automatically discovers plugins in `~/.claude/plugins/`
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
MIT
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
|
+
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const pkg = JSON.parse(
|
|
10
|
+
readFileSync(join(__dirname, '..', 'package.json'), 'utf-8')
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const program = new Command();
|
|
14
|
+
|
|
15
|
+
program
|
|
16
|
+
.name('claude-plugins')
|
|
17
|
+
.description('CLI marketplace for Claude Code plugins')
|
|
18
|
+
.version(pkg.version);
|
|
19
|
+
|
|
20
|
+
program
|
|
21
|
+
.command('search [query]')
|
|
22
|
+
.description('Search plugins by name or keyword')
|
|
23
|
+
.action(async (query) => {
|
|
24
|
+
const { search } = await import('../src/commands/search.js');
|
|
25
|
+
await search(query);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
program
|
|
29
|
+
.command('install <name>')
|
|
30
|
+
.description('Install a plugin from the registry')
|
|
31
|
+
.action(async (name) => {
|
|
32
|
+
const { install } = await import('../src/commands/install.js');
|
|
33
|
+
await install(name);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
program
|
|
37
|
+
.command('uninstall <name>')
|
|
38
|
+
.alias('remove')
|
|
39
|
+
.description('Remove an installed plugin')
|
|
40
|
+
.action(async (name) => {
|
|
41
|
+
const { uninstall } = await import('../src/commands/uninstall.js');
|
|
42
|
+
await uninstall(name);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
program
|
|
46
|
+
.command('list')
|
|
47
|
+
.alias('ls')
|
|
48
|
+
.description('List installed plugins')
|
|
49
|
+
.action(async () => {
|
|
50
|
+
const { list } = await import('../src/commands/list.js');
|
|
51
|
+
await list();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
program
|
|
55
|
+
.command('info <name>')
|
|
56
|
+
.description('Show details about a plugin')
|
|
57
|
+
.action(async (name) => {
|
|
58
|
+
const { info } = await import('../src/commands/info.js');
|
|
59
|
+
await info(name);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
program
|
|
63
|
+
.command('update [name]')
|
|
64
|
+
.alias('upgrade')
|
|
65
|
+
.description('Update one or all installed plugins')
|
|
66
|
+
.action(async (name) => {
|
|
67
|
+
const { update } = await import('../src/commands/update.js');
|
|
68
|
+
await update(name);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
program
|
|
72
|
+
.command('publish')
|
|
73
|
+
.description('Validate current directory and generate registry entry')
|
|
74
|
+
.action(async () => {
|
|
75
|
+
const { publish } = await import('../src/commands/publish.js');
|
|
76
|
+
await publish();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@biggora/claude-plugins",
|
|
3
|
+
"publishConfig": {
|
|
4
|
+
"access": "public"
|
|
5
|
+
},
|
|
6
|
+
"version": "1.0.0",
|
|
7
|
+
"description": "CLI marketplace for discovering, installing, and managing Claude Code plugins",
|
|
8
|
+
"main": "src/index.js",
|
|
9
|
+
"bin": {
|
|
10
|
+
"claude-plugins": "bin/cli.js"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"test": "node bin/cli.js --help"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"claude",
|
|
17
|
+
"claude-code",
|
|
18
|
+
"plugins",
|
|
19
|
+
"marketplace",
|
|
20
|
+
"cli"
|
|
21
|
+
],
|
|
22
|
+
"author": {
|
|
23
|
+
"name": "biggora",
|
|
24
|
+
"url": "https://github.com/biggora"
|
|
25
|
+
},
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/biggora/claude-plugins"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"chalk": "^5.3.0",
|
|
33
|
+
"commander": "^12.1.0",
|
|
34
|
+
"ora": "^8.1.0"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18.0.0"
|
|
38
|
+
},
|
|
39
|
+
"type": "module"
|
|
40
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./schema.json",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"updated": "2026-02-08",
|
|
5
|
+
"plugins": [
|
|
6
|
+
{
|
|
7
|
+
"name": "code-optimizer",
|
|
8
|
+
"version": "1.0.0",
|
|
9
|
+
"description": "Pragmatic code optimization plugin that finds duplicates, extracts constants, and suggests concise solutions without over-engineering",
|
|
10
|
+
"author": {
|
|
11
|
+
"name": "biggora",
|
|
12
|
+
"url": "https://github.com/biggora"
|
|
13
|
+
},
|
|
14
|
+
"repository": "https://github.com/biggora/code-optimizer",
|
|
15
|
+
"keywords": ["optimization", "refactoring", "duplicates", "constants", "code-quality"],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"commands": ["/optimize", "/optimize-project"],
|
|
18
|
+
"category": "code-quality"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "github-issues-solver",
|
|
22
|
+
"version": "1.0.0",
|
|
23
|
+
"description": "Analyze, investigate, and fix GitHub issues with AI assistance. Full workflow from issue to PR.",
|
|
24
|
+
"author": {
|
|
25
|
+
"name": "biggora",
|
|
26
|
+
"url": "https://github.com/biggora"
|
|
27
|
+
},
|
|
28
|
+
"repository": "https://github.com/biggora/github-issues-solver",
|
|
29
|
+
"keywords": ["github", "issues", "bug-fix", "automation", "workflow"],
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"commands": ["/gh-solve-issue", "/gh-list-issues"],
|
|
32
|
+
"category": "workflow"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "typescript-eslint-fixer",
|
|
36
|
+
"version": "1.0.0",
|
|
37
|
+
"description": "Automatically detect and fix TypeScript type errors and ESLint violations in TypeScript/JavaScript projects",
|
|
38
|
+
"author": {
|
|
39
|
+
"name": "biggora",
|
|
40
|
+
"url": "https://github.com/biggora"
|
|
41
|
+
},
|
|
42
|
+
"repository": "https://github.com/biggora/typescript-eslint-fixer",
|
|
43
|
+
"keywords": ["typescript", "eslint", "linting", "code-quality", "auto-fix"],
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"commands": ["/fix-typescript-eslint"],
|
|
46
|
+
"category": "code-quality"
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "Claude Plugins Registry",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": ["version", "plugins"],
|
|
6
|
+
"properties": {
|
|
7
|
+
"version": { "type": "string" },
|
|
8
|
+
"updated": { "type": "string", "format": "date" },
|
|
9
|
+
"plugins": {
|
|
10
|
+
"type": "array",
|
|
11
|
+
"items": {
|
|
12
|
+
"type": "object",
|
|
13
|
+
"required": ["name", "version", "description", "repository"],
|
|
14
|
+
"properties": {
|
|
15
|
+
"name": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"pattern": "^[a-z0-9-]+$",
|
|
18
|
+
"description": "Unique plugin identifier (lowercase, hyphens only)"
|
|
19
|
+
},
|
|
20
|
+
"version": { "type": "string" },
|
|
21
|
+
"description": { "type": "string", "maxLength": 200 },
|
|
22
|
+
"author": {
|
|
23
|
+
"type": "object",
|
|
24
|
+
"properties": {
|
|
25
|
+
"name": { "type": "string" },
|
|
26
|
+
"url": { "type": "string", "format": "uri" }
|
|
27
|
+
},
|
|
28
|
+
"required": ["name"]
|
|
29
|
+
},
|
|
30
|
+
"repository": { "type": "string", "format": "uri" },
|
|
31
|
+
"keywords": {
|
|
32
|
+
"type": "array",
|
|
33
|
+
"items": { "type": "string" },
|
|
34
|
+
"maxItems": 10
|
|
35
|
+
},
|
|
36
|
+
"license": { "type": "string" },
|
|
37
|
+
"commands": {
|
|
38
|
+
"type": "array",
|
|
39
|
+
"items": { "type": "string" }
|
|
40
|
+
},
|
|
41
|
+
"category": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"enum": ["code-quality", "workflow", "testing", "documentation", "security", "devops", "other"]
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { getPluginsDir } from '../config.js';
|
|
5
|
+
import { fetchRegistry, findPlugin } from '../registry.js';
|
|
6
|
+
import { log } from '../utils.js';
|
|
7
|
+
|
|
8
|
+
export async function info(name) {
|
|
9
|
+
const registry = await fetchRegistry();
|
|
10
|
+
const plugin = findPlugin(registry, name);
|
|
11
|
+
|
|
12
|
+
if (!plugin) {
|
|
13
|
+
log.error(`Plugin "${name}" not found in registry`);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const installed = existsSync(join(getPluginsDir(), plugin.name));
|
|
18
|
+
|
|
19
|
+
console.log();
|
|
20
|
+
console.log(chalk.bold.cyan(` ${plugin.name}`) + chalk.dim(` v${plugin.version}`));
|
|
21
|
+
console.log();
|
|
22
|
+
console.log(` ${plugin.description}`);
|
|
23
|
+
console.log();
|
|
24
|
+
|
|
25
|
+
const fields = [
|
|
26
|
+
['Author', plugin.author?.name || '-'],
|
|
27
|
+
['License', plugin.license || '-'],
|
|
28
|
+
['Category', plugin.category || '-'],
|
|
29
|
+
['Repository', plugin.repository || '-'],
|
|
30
|
+
['Keywords', (plugin.keywords || []).join(', ') || '-'],
|
|
31
|
+
['Commands', (plugin.commands || []).join(', ') || '-'],
|
|
32
|
+
['Installed', installed ? chalk.green('yes') : chalk.dim('no')],
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
for (const [label, value] of fields) {
|
|
36
|
+
console.log(` ${chalk.dim(label.padEnd(12))} ${value}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log();
|
|
40
|
+
|
|
41
|
+
if (!installed) {
|
|
42
|
+
log.dim(` Run "claude-plugins install ${plugin.name}" to install`);
|
|
43
|
+
console.log();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { getPluginsDir } from '../config.js';
|
|
5
|
+
import { fetchRegistry, findPlugin } from '../registry.js';
|
|
6
|
+
import { log, spinner } from '../utils.js';
|
|
7
|
+
|
|
8
|
+
export async function install(name) {
|
|
9
|
+
const registry = await fetchRegistry();
|
|
10
|
+
const plugin = findPlugin(registry, name);
|
|
11
|
+
|
|
12
|
+
if (!plugin) {
|
|
13
|
+
log.error(`Plugin "${name}" not found in registry`);
|
|
14
|
+
log.dim('Run "claude-plugins search <query>" to find plugins');
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const pluginsDir = getPluginsDir();
|
|
19
|
+
const dest = join(pluginsDir, plugin.name);
|
|
20
|
+
|
|
21
|
+
if (existsSync(dest)) {
|
|
22
|
+
log.warn(`Plugin "${plugin.name}" is already installed at ${dest}`);
|
|
23
|
+
log.dim('Run "claude-plugins update ' + plugin.name + '" to update it');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const spin = spinner(`Installing ${plugin.name}...`);
|
|
28
|
+
spin.start();
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
execFileSync('git', ['clone', `${plugin.repository}.git`, dest], {
|
|
32
|
+
stdio: 'pipe',
|
|
33
|
+
});
|
|
34
|
+
spin.succeed(`Installed ${plugin.name} v${plugin.version}`);
|
|
35
|
+
log.dim(` ${dest}`);
|
|
36
|
+
log.dim(` ${plugin.description}`);
|
|
37
|
+
|
|
38
|
+
if (plugin.commands?.length) {
|
|
39
|
+
log.info(`Commands: ${plugin.commands.join(', ')}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
log.dim('\nRestart Claude Code to load the plugin.');
|
|
43
|
+
} catch (err) {
|
|
44
|
+
spin.fail(`Failed to install ${plugin.name}`);
|
|
45
|
+
log.error(err.message);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { readdirSync, existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { getPluginsDir } from '../config.js';
|
|
5
|
+
import { log, formatTable, truncate } from '../utils.js';
|
|
6
|
+
|
|
7
|
+
export async function list() {
|
|
8
|
+
const pluginsDir = getPluginsDir();
|
|
9
|
+
const entries = readdirSync(pluginsDir, { withFileTypes: true }).filter(
|
|
10
|
+
(e) => e.isDirectory()
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
if (!entries.length) {
|
|
14
|
+
log.info('No plugins installed');
|
|
15
|
+
log.dim('Run "claude-plugins search" to browse available plugins');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
console.log(chalk.bold(`\n ${entries.length} plugin${entries.length === 1 ? '' : 's'} installed\n`));
|
|
20
|
+
|
|
21
|
+
const rows = entries.map((entry) => {
|
|
22
|
+
const dir = join(pluginsDir, entry.name);
|
|
23
|
+
const manifestPath = join(dir, '.claude-plugin', 'plugin.json');
|
|
24
|
+
let version = '-';
|
|
25
|
+
let description = '';
|
|
26
|
+
|
|
27
|
+
if (existsSync(manifestPath)) {
|
|
28
|
+
try {
|
|
29
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
30
|
+
version = manifest.version || '-';
|
|
31
|
+
description = manifest.description || '';
|
|
32
|
+
} catch {
|
|
33
|
+
// ignore parse errors
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return [entry.name, truncate(description, 55), version];
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
formatTable(rows, ['Name', 'Description', 'Version']);
|
|
41
|
+
console.log();
|
|
42
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { log } from '../utils.js';
|
|
6
|
+
|
|
7
|
+
const REQUIRED_FILES = [
|
|
8
|
+
'.claude-plugin/plugin.json',
|
|
9
|
+
'README.md',
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
export async function publish() {
|
|
13
|
+
const cwd = process.cwd();
|
|
14
|
+
const errors = [];
|
|
15
|
+
const warnings = [];
|
|
16
|
+
|
|
17
|
+
// Check required files
|
|
18
|
+
for (const file of REQUIRED_FILES) {
|
|
19
|
+
if (!existsSync(join(cwd, file))) {
|
|
20
|
+
errors.push(`Missing required file: ${file}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (errors.length) {
|
|
25
|
+
log.error('Plugin validation failed:\n');
|
|
26
|
+
errors.forEach((e) => console.log(chalk.red(` - ${e}`)));
|
|
27
|
+
console.log();
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Read and validate plugin.json
|
|
32
|
+
let manifest;
|
|
33
|
+
try {
|
|
34
|
+
manifest = JSON.parse(
|
|
35
|
+
readFileSync(join(cwd, '.claude-plugin/plugin.json'), 'utf-8')
|
|
36
|
+
);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
log.error('Invalid plugin.json: ' + err.message);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const required = ['name', 'version', 'description'];
|
|
43
|
+
for (const field of required) {
|
|
44
|
+
if (!manifest[field]) {
|
|
45
|
+
errors.push(`plugin.json missing required field: "${field}"`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!/^[a-z0-9-]+$/.test(manifest.name || '')) {
|
|
50
|
+
errors.push('plugin.json "name" must be lowercase with hyphens only');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check for recommended fields
|
|
54
|
+
if (!manifest.repository) {
|
|
55
|
+
warnings.push('plugin.json missing "repository" field');
|
|
56
|
+
}
|
|
57
|
+
if (!manifest.keywords?.length) {
|
|
58
|
+
warnings.push('plugin.json missing "keywords" - helps with search');
|
|
59
|
+
}
|
|
60
|
+
if (!manifest.license) {
|
|
61
|
+
warnings.push('plugin.json missing "license" field');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check for git remote
|
|
65
|
+
let repoUrl = manifest.repository || '';
|
|
66
|
+
if (!repoUrl) {
|
|
67
|
+
try {
|
|
68
|
+
repoUrl = execFileSync('git', ['remote', 'get-url', 'origin'], {
|
|
69
|
+
cwd,
|
|
70
|
+
stdio: 'pipe',
|
|
71
|
+
encoding: 'utf-8',
|
|
72
|
+
}).trim();
|
|
73
|
+
} catch {
|
|
74
|
+
warnings.push('No git remote found - you will need a GitHub repository');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (errors.length) {
|
|
79
|
+
log.error('Plugin validation failed:\n');
|
|
80
|
+
errors.forEach((e) => console.log(chalk.red(` - ${e}`)));
|
|
81
|
+
console.log();
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Output result
|
|
86
|
+
console.log();
|
|
87
|
+
log.success('Plugin validation passed!\n');
|
|
88
|
+
|
|
89
|
+
if (warnings.length) {
|
|
90
|
+
warnings.forEach((w) => log.warn(w));
|
|
91
|
+
console.log();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
console.log(chalk.bold(' Plugin Details:\n'));
|
|
95
|
+
console.log(` ${chalk.dim('Name')} ${manifest.name}`);
|
|
96
|
+
console.log(` ${chalk.dim('Version')} ${manifest.version}`);
|
|
97
|
+
console.log(` ${chalk.dim('Description')} ${manifest.description}`);
|
|
98
|
+
if (repoUrl) {
|
|
99
|
+
console.log(` ${chalk.dim('Repository')} ${repoUrl}`);
|
|
100
|
+
}
|
|
101
|
+
console.log();
|
|
102
|
+
|
|
103
|
+
// Generate registry entry
|
|
104
|
+
const entry = {
|
|
105
|
+
name: manifest.name,
|
|
106
|
+
version: manifest.version,
|
|
107
|
+
description: manifest.description,
|
|
108
|
+
author: manifest.author || { name: 'unknown' },
|
|
109
|
+
repository: repoUrl.replace(/\.git$/, ''),
|
|
110
|
+
keywords: manifest.keywords || [],
|
|
111
|
+
license: manifest.license || 'MIT',
|
|
112
|
+
commands: manifest.commands || [],
|
|
113
|
+
category: manifest.category || 'other',
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
console.log(chalk.bold(' Registry entry (add to registry.json):\n'));
|
|
117
|
+
console.log(chalk.dim(' ' + JSON.stringify(entry, null, 2).split('\n').join('\n ')));
|
|
118
|
+
console.log();
|
|
119
|
+
log.info(
|
|
120
|
+
'To publish, submit a PR to:\n https://github.com/biggora/claude-plugins-registry'
|
|
121
|
+
);
|
|
122
|
+
log.dim(' Add the entry above to the "plugins" array in registry.json');
|
|
123
|
+
console.log();
|
|
124
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { getPluginsDir } from '../config.js';
|
|
5
|
+
import { fetchRegistry, searchPlugins } from '../registry.js';
|
|
6
|
+
import { log, formatTable, truncate } from '../utils.js';
|
|
7
|
+
|
|
8
|
+
export async function search(query) {
|
|
9
|
+
const registry = await fetchRegistry();
|
|
10
|
+
const results = query
|
|
11
|
+
? searchPlugins(registry, query)
|
|
12
|
+
: registry.plugins;
|
|
13
|
+
|
|
14
|
+
if (!results.length) {
|
|
15
|
+
log.warn(`No plugins found for "${query}"`);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const pluginsDir = getPluginsDir();
|
|
20
|
+
|
|
21
|
+
console.log(
|
|
22
|
+
chalk.bold(`\n ${results.length} plugin${results.length === 1 ? '' : 's'} found\n`)
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const rows = results.map((p) => {
|
|
26
|
+
const installed = existsSync(join(pluginsDir, p.name));
|
|
27
|
+
const status = installed ? chalk.green('installed') : '';
|
|
28
|
+
return [p.name, truncate(p.description, 55), p.version, status];
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
formatTable(rows, ['Name', 'Description', 'Version', 'Status']);
|
|
32
|
+
console.log();
|
|
33
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { existsSync, rmSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { getPluginsDir } from '../config.js';
|
|
4
|
+
import { log, spinner } from '../utils.js';
|
|
5
|
+
|
|
6
|
+
export async function uninstall(name) {
|
|
7
|
+
const dest = join(getPluginsDir(), name);
|
|
8
|
+
|
|
9
|
+
if (!existsSync(dest)) {
|
|
10
|
+
log.error(`Plugin "${name}" is not installed`);
|
|
11
|
+
log.dim('Run "claude-plugins list" to see installed plugins');
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const spin = spinner(`Uninstalling ${name}...`);
|
|
16
|
+
spin.start();
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
rmSync(dest, { recursive: true, force: true });
|
|
20
|
+
spin.succeed(`Uninstalled ${name}`);
|
|
21
|
+
log.dim('Restart Claude Code to apply changes.');
|
|
22
|
+
} catch (err) {
|
|
23
|
+
spin.fail(`Failed to uninstall ${name}`);
|
|
24
|
+
log.error(err.message);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { getPluginsDir } from '../config.js';
|
|
5
|
+
import { log, spinner } from '../utils.js';
|
|
6
|
+
|
|
7
|
+
function updateOne(name, pluginsDir) {
|
|
8
|
+
const dest = join(pluginsDir, name);
|
|
9
|
+
|
|
10
|
+
if (!existsSync(dest)) {
|
|
11
|
+
log.error(`Plugin "${name}" is not installed`);
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const gitDir = join(dest, '.git');
|
|
16
|
+
if (!existsSync(gitDir)) {
|
|
17
|
+
log.warn(`Plugin "${name}" was not installed via git, skipping`);
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const spin = spinner(`Updating ${name}...`);
|
|
22
|
+
spin.start();
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const output = execFileSync('git', ['pull', '--ff-only'], {
|
|
26
|
+
cwd: dest,
|
|
27
|
+
stdio: 'pipe',
|
|
28
|
+
encoding: 'utf-8',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (output.includes('Already up to date')) {
|
|
32
|
+
spin.info(`${name} is already up to date`);
|
|
33
|
+
} else {
|
|
34
|
+
spin.succeed(`Updated ${name}`);
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
} catch (err) {
|
|
38
|
+
spin.fail(`Failed to update ${name}`);
|
|
39
|
+
log.error(err.message);
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function update(name) {
|
|
45
|
+
const pluginsDir = getPluginsDir();
|
|
46
|
+
|
|
47
|
+
if (name) {
|
|
48
|
+
updateOne(name, pluginsDir);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Update all installed plugins
|
|
53
|
+
const entries = readdirSync(pluginsDir, { withFileTypes: true }).filter(
|
|
54
|
+
(e) => e.isDirectory()
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
if (!entries.length) {
|
|
58
|
+
log.info('No plugins installed');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
log.info(`Updating ${entries.length} plugin${entries.length === 1 ? '' : 's'}...\n`);
|
|
63
|
+
|
|
64
|
+
let updated = 0;
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
if (updateOne(entry.name, pluginsDir)) updated++;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log();
|
|
70
|
+
log.success(`${updated}/${entries.length} plugins updated`);
|
|
71
|
+
log.dim('Restart Claude Code to apply changes.');
|
|
72
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
4
|
+
|
|
5
|
+
const home = homedir();
|
|
6
|
+
const isWindows = process.platform === 'win32';
|
|
7
|
+
|
|
8
|
+
export const PLUGINS_DIR = join(home, '.claude', 'plugins');
|
|
9
|
+
export const CACHE_DIR = join(home, '.claude', '.cache', 'claude-plugins');
|
|
10
|
+
export const CACHE_TTL = 1000 * 60 * 15; // 15 minutes
|
|
11
|
+
export const REGISTRY_URL =
|
|
12
|
+
'https://raw.githubusercontent.com/biggora/claude-plugins-registry/main/registry/registry.json';
|
|
13
|
+
|
|
14
|
+
export function ensureDir(dir) {
|
|
15
|
+
if (!existsSync(dir)) {
|
|
16
|
+
mkdirSync(dir, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
return dir;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getPluginsDir() {
|
|
22
|
+
return ensureDir(PLUGINS_DIR);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getCacheDir() {
|
|
26
|
+
return ensureDir(CACHE_DIR);
|
|
27
|
+
}
|
package/src/registry.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { getCacheDir, CACHE_TTL, REGISTRY_URL } from './config.js';
|
|
4
|
+
import { log } from './utils.js';
|
|
5
|
+
|
|
6
|
+
const CACHE_FILE = 'registry.json';
|
|
7
|
+
|
|
8
|
+
function getCachePath() {
|
|
9
|
+
return join(getCacheDir(), CACHE_FILE);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isCacheValid() {
|
|
13
|
+
const cachePath = getCachePath();
|
|
14
|
+
if (!existsSync(cachePath)) return false;
|
|
15
|
+
const stat = statSync(cachePath);
|
|
16
|
+
return Date.now() - stat.mtimeMs < CACHE_TTL;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function fetchRegistry({ force = false } = {}) {
|
|
20
|
+
const cachePath = getCachePath();
|
|
21
|
+
|
|
22
|
+
if (!force && isCacheValid()) {
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(readFileSync(cachePath, 'utf-8'));
|
|
25
|
+
} catch {
|
|
26
|
+
// Cache corrupted, refetch
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const res = await fetch(REGISTRY_URL);
|
|
31
|
+
if (!res.ok) {
|
|
32
|
+
// Fall back to cache if available
|
|
33
|
+
if (existsSync(cachePath)) {
|
|
34
|
+
log.warn('Could not fetch registry, using cached version');
|
|
35
|
+
return JSON.parse(readFileSync(cachePath, 'utf-8'));
|
|
36
|
+
}
|
|
37
|
+
// Fall back to bundled registry
|
|
38
|
+
const bundledPath = new URL('../registry/registry.json', import.meta.url);
|
|
39
|
+
log.warn('Could not fetch registry, using bundled version');
|
|
40
|
+
return JSON.parse(readFileSync(bundledPath, 'utf-8'));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const data = await res.json();
|
|
44
|
+
writeFileSync(cachePath, JSON.stringify(data, null, 2));
|
|
45
|
+
return data;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function searchPlugins(registry, query) {
|
|
49
|
+
const q = query.toLowerCase();
|
|
50
|
+
return registry.plugins.filter((p) => {
|
|
51
|
+
const haystack = [
|
|
52
|
+
p.name,
|
|
53
|
+
p.description,
|
|
54
|
+
...(p.keywords || []),
|
|
55
|
+
p.author?.name || '',
|
|
56
|
+
]
|
|
57
|
+
.join(' ')
|
|
58
|
+
.toLowerCase();
|
|
59
|
+
return haystack.includes(q);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function findPlugin(registry, name) {
|
|
64
|
+
return registry.plugins.find(
|
|
65
|
+
(p) => p.name.toLowerCase() === name.toLowerCase()
|
|
66
|
+
);
|
|
67
|
+
}
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
|
|
4
|
+
export const log = {
|
|
5
|
+
info: (msg) => console.log(chalk.blue('i'), msg),
|
|
6
|
+
success: (msg) => console.log(chalk.green('\u2713'), msg),
|
|
7
|
+
warn: (msg) => console.log(chalk.yellow('!'), msg),
|
|
8
|
+
error: (msg) => console.error(chalk.red('\u2717'), msg),
|
|
9
|
+
dim: (msg) => console.log(chalk.dim(msg)),
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function spinner(text) {
|
|
13
|
+
return ora({ text, color: 'cyan' });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function formatTable(rows, headers) {
|
|
17
|
+
const cols = headers.length;
|
|
18
|
+
const widths = headers.map((h, i) =>
|
|
19
|
+
Math.max(h.length, ...rows.map((r) => String(r[i] || '').length))
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const sep = widths.map((w) => '-'.repeat(w)).join('--+-');
|
|
23
|
+
const formatRow = (row) =>
|
|
24
|
+
row.map((cell, i) => String(cell || '').padEnd(widths[i])).join(' | ');
|
|
25
|
+
|
|
26
|
+
console.log(chalk.bold(formatRow(headers)));
|
|
27
|
+
console.log(chalk.dim(sep));
|
|
28
|
+
rows.forEach((row) => console.log(formatRow(row)));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function truncate(str, len = 60) {
|
|
32
|
+
if (!str) return '';
|
|
33
|
+
return str.length > len ? str.slice(0, len - 3) + '...' : str;
|
|
34
|
+
}
|