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