claudepluginhub 0.3.2 → 0.4.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 +58 -9
- package/dist/detect.d.ts +28 -0
- package/dist/detect.js +152 -0
- package/dist/github-install.d.ts +3 -0
- package/dist/github-install.js +214 -0
- package/dist/github.d.ts +2 -3
- package/dist/index.js +25 -7
- package/dist/native.d.ts +18 -0
- package/dist/native.js +42 -0
- package/dist/output.d.ts +1 -0
- package/dist/output.js +3 -0
- package/dist/wrapper.d.ts +25 -0
- package/dist/wrapper.js +113 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,28 +1,77 @@
|
|
|
1
1
|
# claudepluginhub
|
|
2
2
|
|
|
3
|
-
Install Claude Code plugins from [ClaudePluginHub](https://claudepluginhub.com) with a single command.
|
|
3
|
+
Install Claude Code plugins from [ClaudePluginHub](https://claudepluginhub.com) and GitHub with a single command.
|
|
4
4
|
|
|
5
5
|
## Usage
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
+
# GitHub plugin or marketplace
|
|
9
|
+
npx claudepluginhub owner/repo
|
|
10
|
+
|
|
11
|
+
# ClaudePluginHub collection
|
|
8
12
|
npx claudepluginhub u/<userId>/<slug>
|
|
13
|
+
|
|
14
|
+
# Full URL
|
|
15
|
+
npx claudepluginhub https://claudepluginhub.com/api/user-plugins/<userId>/<slug>/marketplace.json
|
|
9
16
|
```
|
|
10
17
|
|
|
11
|
-
|
|
18
|
+
## Identifier types
|
|
19
|
+
|
|
20
|
+
### `owner/repo` (GitHub)
|
|
21
|
+
|
|
22
|
+
Installs a plugin or marketplace directly from a GitHub repository using Claude Code's native plugin system.
|
|
12
23
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
24
|
+
- **Marketplace repos** (`.claude-plugin/marketplace.json`): Adds the marketplace and installs selected plugins via `claude plugin marketplace add` and `claude plugin install`.
|
|
25
|
+
- **Plugin repos** (with or without `.claude-plugin/plugin.json`): Creates a local wrapper marketplace, then installs via Claude Code.
|
|
26
|
+
|
|
27
|
+
### `u/<userId>/<slug>` (ClaudePluginHub)
|
|
28
|
+
|
|
29
|
+
Installs from a ClaudePluginHub user-curated collection. Components are downloaded directly and tracked in a lock file.
|
|
16
30
|
|
|
17
31
|
### Full URL
|
|
18
32
|
|
|
19
|
-
|
|
33
|
+
Same as `u/` form but with the full marketplace JSON URL.
|
|
34
|
+
|
|
35
|
+
## Commands
|
|
20
36
|
|
|
21
|
-
```bash
|
|
22
|
-
npx claudepluginhub https://claudepluginhub.com/api/user-plugins/<userId>/<slug>/marketplace.json
|
|
23
37
|
```
|
|
38
|
+
npx claudepluginhub <identifier> Install from a source
|
|
39
|
+
npx claudepluginhub add <identifier> Same as above
|
|
40
|
+
npx claudepluginhub update [identifier] Update (u/ collections only)
|
|
41
|
+
npx claudepluginhub list List (u/ collections only)
|
|
42
|
+
npx claudepluginhub remove <identifier> Remove
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Options
|
|
46
|
+
|
|
47
|
+
| Option | Description |
|
|
48
|
+
|--------|-------------|
|
|
49
|
+
| `--yes, -y` | Skip prompts (install all, project scope) |
|
|
50
|
+
| `--scope <scope>` | Set scope: `user`, `project` (default), `local` |
|
|
51
|
+
| `-h, --help` | Show help |
|
|
52
|
+
|
|
53
|
+
## Lifecycle differences
|
|
54
|
+
|
|
55
|
+
| Feature | `owner/repo` | `u/` collections |
|
|
56
|
+
|---------|-------------|------------------|
|
|
57
|
+
| Install | `claude plugin` commands | Direct download |
|
|
58
|
+
| List | `claude plugin list` | `npx claudepluginhub list` |
|
|
59
|
+
| Update | `claude plugin update` | `npx claudepluginhub update` |
|
|
60
|
+
| Remove | `npx claudepluginhub remove owner/repo --scope <scope>` (standalone) or `claude plugin` commands (marketplace) | `npx claudepluginhub remove` |
|
|
61
|
+
|
|
62
|
+
## Standalone wrapper
|
|
63
|
+
|
|
64
|
+
When installing a standalone plugin via `owner/repo`, a local wrapper marketplace is created:
|
|
65
|
+
|
|
66
|
+
| Scope | Wrapper path |
|
|
67
|
+
|-------|-------------|
|
|
68
|
+
| `user` | `~/.claude/.cpd-wrappers/<owner>-<repo>-user/` |
|
|
69
|
+
| `project` | `.claude/.cpd-wrappers/<owner>-<repo>-project/` |
|
|
70
|
+
| `local` | `~/.claude/.cpd-wrappers/<owner>-<repo>-local-<cwd-hash>/` |
|
|
71
|
+
|
|
72
|
+
**Note:** `local` scope wrappers are stored under `~/.claude/` and are tied to the working directory where they were installed. Running `npx claudepluginhub remove owner/repo --scope local` for a local install must be done from the same project directory.
|
|
24
73
|
|
|
25
74
|
## Requirements
|
|
26
75
|
|
|
27
76
|
- Node.js 18+
|
|
28
|
-
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview) installed and available in PATH
|
|
77
|
+
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview) installed and available in PATH (required for `owner/repo` installs)
|
package/dist/detect.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { TreeEntry } from './github.js';
|
|
2
|
+
export declare function isOwnerRepo(identifier: string): boolean;
|
|
3
|
+
export interface MarketplaceDetection {
|
|
4
|
+
kind: 'marketplace';
|
|
5
|
+
manifest: {
|
|
6
|
+
name: string;
|
|
7
|
+
plugins: Array<{
|
|
8
|
+
name: string;
|
|
9
|
+
description: string | null;
|
|
10
|
+
source: {
|
|
11
|
+
source: string;
|
|
12
|
+
repo: string;
|
|
13
|
+
};
|
|
14
|
+
strict: boolean;
|
|
15
|
+
}>;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export interface PluginDetection {
|
|
19
|
+
kind: 'plugin';
|
|
20
|
+
name: string;
|
|
21
|
+
strict: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface UnknownDetection {
|
|
24
|
+
kind: 'unknown';
|
|
25
|
+
}
|
|
26
|
+
export type RepoDetection = MarketplaceDetection | PluginDetection | UnknownDetection;
|
|
27
|
+
export declare function hasComponentFiles(tree: TreeEntry[], prefix?: string): boolean;
|
|
28
|
+
export declare function detectRepo(ownerRepo: string): Promise<RepoDetection>;
|
package/dist/detect.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { fetchDefaultBranch, fetchTree, downloadFile } from './github.js';
|
|
2
|
+
import { printError } from './output.js';
|
|
3
|
+
import { sanitizeName } from './wrapper.js';
|
|
4
|
+
const OWNER_REPO_RE = /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/;
|
|
5
|
+
const VALID_NAME_RE = /^[a-zA-Z0-9_.-]+$/;
|
|
6
|
+
export function isOwnerRepo(identifier) {
|
|
7
|
+
if (!OWNER_REPO_RE.test(identifier))
|
|
8
|
+
return false;
|
|
9
|
+
const [owner, repo] = identifier.split('/');
|
|
10
|
+
// Reject segments that are purely dots (e.g. "..", ".")
|
|
11
|
+
if (/^\.+$/.test(owner) || /^\.+$/.test(repo))
|
|
12
|
+
return false;
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
const COMPONENT_PATTERNS = [
|
|
16
|
+
/^commands\/.*\.md$/,
|
|
17
|
+
/^agents\/.*\.md$/,
|
|
18
|
+
/^skills\/.*\/SKILL\.md$/,
|
|
19
|
+
/^hooks\/hooks\.json$/,
|
|
20
|
+
/^\.mcp\.json$/,
|
|
21
|
+
/^\.lsp\.json$/,
|
|
22
|
+
];
|
|
23
|
+
export function hasComponentFiles(tree, prefix) {
|
|
24
|
+
for (const entry of tree) {
|
|
25
|
+
if (entry.type !== 'blob')
|
|
26
|
+
continue;
|
|
27
|
+
const path = prefix ? entry.path.slice(prefix.length) : entry.path;
|
|
28
|
+
if (COMPONENT_PATTERNS.some((p) => p.test(path)))
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
export async function detectRepo(ownerRepo) {
|
|
34
|
+
const branch = await fetchDefaultBranch(ownerRepo);
|
|
35
|
+
if (!branch) {
|
|
36
|
+
printError(`Could not access repository ${ownerRepo}`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
const tree = await fetchTree(ownerRepo, branch);
|
|
40
|
+
if (!tree) {
|
|
41
|
+
printError(`Could not fetch file tree for ${ownerRepo}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
if (tree.truncated) {
|
|
45
|
+
printError(`Repository tree is too large (truncated). Cannot detect plugin structure.`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
const hasMarketplace = tree.tree.some((e) => e.type === 'blob' && e.path === '.claude-plugin/marketplace.json');
|
|
49
|
+
const hasPluginJson = tree.tree.some((e) => e.type === 'blob' && e.path === '.claude-plugin/plugin.json');
|
|
50
|
+
// Marketplace repo
|
|
51
|
+
if (hasMarketplace) {
|
|
52
|
+
const content = await downloadFile(ownerRepo, branch, '.claude-plugin/marketplace.json');
|
|
53
|
+
if (!content) {
|
|
54
|
+
printError('Failed to download marketplace.json');
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
let manifest;
|
|
58
|
+
try {
|
|
59
|
+
manifest = JSON.parse(content);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
printError('marketplace.json is not valid JSON');
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
if (!manifest.name || typeof manifest.name !== 'string') {
|
|
66
|
+
printError('marketplace.json is missing the required "name" field');
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
if (!VALID_NAME_RE.test(manifest.name)) {
|
|
70
|
+
printError(`marketplace.json has an invalid name: "${manifest.name}". Must match [a-zA-Z0-9_.-]+`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
const rawPlugins = manifest.plugins;
|
|
74
|
+
if (!Array.isArray(rawPlugins)) {
|
|
75
|
+
printError('marketplace.json "plugins" must be an array');
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
const validPlugins = [];
|
|
79
|
+
for (const p of rawPlugins) {
|
|
80
|
+
if (!p || typeof p !== 'object') {
|
|
81
|
+
printError('marketplace.json contains a non-object plugin entry');
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
const plugin = p;
|
|
85
|
+
const name = plugin.name;
|
|
86
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
87
|
+
printError(`marketplace.json plugin is missing the required "name" field`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
if (!VALID_NAME_RE.test(name)) {
|
|
91
|
+
printError(`marketplace.json plugin has an invalid name: "${name}". Must match [a-zA-Z0-9_.-]+`);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
const rawSource = plugin.source;
|
|
95
|
+
const source = rawSource &&
|
|
96
|
+
typeof rawSource === 'object' &&
|
|
97
|
+
typeof rawSource.source === 'string' &&
|
|
98
|
+
typeof rawSource.repo === 'string'
|
|
99
|
+
? rawSource
|
|
100
|
+
: { source: 'github', repo: ownerRepo };
|
|
101
|
+
validPlugins.push({
|
|
102
|
+
name,
|
|
103
|
+
description: typeof plugin.description === 'string' ? plugin.description : null,
|
|
104
|
+
source,
|
|
105
|
+
strict: plugin.strict === false ? false : true,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
kind: 'marketplace',
|
|
110
|
+
manifest: {
|
|
111
|
+
name: manifest.name,
|
|
112
|
+
plugins: validPlugins,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
// Plugin with manifest
|
|
117
|
+
if (hasPluginJson) {
|
|
118
|
+
const content = await downloadFile(ownerRepo, branch, '.claude-plugin/plugin.json');
|
|
119
|
+
if (!content) {
|
|
120
|
+
printError('Failed to download plugin.json');
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
let pluginJson;
|
|
124
|
+
try {
|
|
125
|
+
pluginJson = JSON.parse(content);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
printError('plugin.json is not valid JSON');
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
if (!('name' in pluginJson) || typeof pluginJson.name !== 'string') {
|
|
132
|
+
printError('plugin.json is missing the required "name" field');
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
if (!VALID_NAME_RE.test(pluginJson.name)) {
|
|
136
|
+
printError(`plugin.json has an invalid name: "${pluginJson.name}". Must match [a-zA-Z0-9_.-]+`);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
const strict = pluginJson.strict !== false;
|
|
140
|
+
return { kind: 'plugin', name: pluginJson.name, strict };
|
|
141
|
+
}
|
|
142
|
+
// Plugin without manifest — check for component files at root
|
|
143
|
+
if (hasComponentFiles(tree.tree)) {
|
|
144
|
+
const repo = ownerRepo.split('/')[1];
|
|
145
|
+
return {
|
|
146
|
+
kind: 'plugin',
|
|
147
|
+
name: sanitizeName(repo),
|
|
148
|
+
strict: false,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return { kind: 'unknown' };
|
|
152
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { scopePrompt, checkboxPrompt } from './prompts.js';
|
|
2
|
+
import { detectRepo } from './detect.js';
|
|
3
|
+
import { checkClaudeCli, claudeMarketplaceAdd, claudePluginInstall, claudePluginUninstall, claudeMarketplaceRemove, } from './native.js';
|
|
4
|
+
import { writeWrapperMarketplace, readWrapperMeta, findWrapperScopes, removeWrapperDir, getWrapperDir, } from './wrapper.js';
|
|
5
|
+
import { print, printStep, printSuccess, printFail, printError, printWarning, BOLD, RESET, DIM, GREEN, YELLOW, } from './output.js';
|
|
6
|
+
export async function runGithubInstall(ownerRepo, yes, scopeOverride) {
|
|
7
|
+
if (!checkClaudeCli())
|
|
8
|
+
process.exit(1);
|
|
9
|
+
printStep('Detecting repository type...');
|
|
10
|
+
const detection = await detectRepo(ownerRepo);
|
|
11
|
+
if (detection.kind === 'unknown') {
|
|
12
|
+
printError(`No plugin or marketplace found in ${ownerRepo}. Expected .claude-plugin/ directory or component files (commands/, agents/, skills/, hooks/, .mcp.json, .lsp.json).`);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
if (detection.kind === 'marketplace') {
|
|
16
|
+
await installMarketplace(ownerRepo, detection, yes, scopeOverride);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
await installStandalone(ownerRepo, detection, yes, scopeOverride);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async function installMarketplace(ownerRepo, detection, yes, scopeOverride) {
|
|
23
|
+
const { manifest } = detection;
|
|
24
|
+
printSuccess(`Marketplace "${manifest.name}" — ${manifest.plugins.length} plugin(s)`);
|
|
25
|
+
if (manifest.plugins.length === 0) {
|
|
26
|
+
printError('No valid plugins found in marketplace.');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
// Plugin picker
|
|
30
|
+
let selectedPlugins = manifest.plugins;
|
|
31
|
+
if (!yes && manifest.plugins.length > 1) {
|
|
32
|
+
print('');
|
|
33
|
+
const items = manifest.plugins.map((p) => ({
|
|
34
|
+
label: p.name,
|
|
35
|
+
description: p.description ? `${DIM}${p.description}${RESET}` : undefined,
|
|
36
|
+
selected: true,
|
|
37
|
+
}));
|
|
38
|
+
const chosen = await checkboxPrompt('Select plugins to install:', items);
|
|
39
|
+
if (chosen === null) {
|
|
40
|
+
print('\nCancelled.');
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
selectedPlugins = chosen.map((i) => manifest.plugins[i]);
|
|
44
|
+
if (selectedPlugins.length === 0) {
|
|
45
|
+
printError('No plugins selected.');
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Scope picker
|
|
50
|
+
let scope = scopeOverride ?? 'project';
|
|
51
|
+
if (!yes && !scopeOverride) {
|
|
52
|
+
print('');
|
|
53
|
+
const chosenScope = await scopePrompt('project');
|
|
54
|
+
if (chosenScope === null) {
|
|
55
|
+
print('\nCancelled.');
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
scope = chosenScope;
|
|
59
|
+
}
|
|
60
|
+
// Add marketplace
|
|
61
|
+
print('');
|
|
62
|
+
printStep(`Adding marketplace from ${ownerRepo}...`);
|
|
63
|
+
const addResult = claudeMarketplaceAdd(ownerRepo, scope);
|
|
64
|
+
if (!addResult.ok) {
|
|
65
|
+
printError(`Failed to add marketplace: ${addResult.output}`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
printSuccess('Marketplace added');
|
|
69
|
+
// Install each selected plugin
|
|
70
|
+
let failures = 0;
|
|
71
|
+
for (const plugin of selectedPlugins) {
|
|
72
|
+
printStep(`Installing ${plugin.name}...`);
|
|
73
|
+
const result = claudePluginInstall(plugin.name, manifest.name, scope);
|
|
74
|
+
if (result.ok) {
|
|
75
|
+
printSuccess(plugin.name);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
printFail(plugin.name, result.output);
|
|
79
|
+
failures++;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
print('');
|
|
83
|
+
if (failures === 0) {
|
|
84
|
+
print(`${GREEN}${BOLD}Done!${RESET} ${selectedPlugins.length} plugin(s) installed.`);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
print(`${YELLOW}${BOLD}Done.${RESET} ${selectedPlugins.length - failures} succeeded, ${failures} failed.`);
|
|
88
|
+
}
|
|
89
|
+
print('');
|
|
90
|
+
printNativeGuidance();
|
|
91
|
+
}
|
|
92
|
+
async function installStandalone(ownerRepo, detection, yes, scopeOverride) {
|
|
93
|
+
printSuccess(`Plugin "${detection.name}" (strict: ${detection.strict})`);
|
|
94
|
+
// Scope picker
|
|
95
|
+
let scope = scopeOverride ?? 'project';
|
|
96
|
+
if (!yes && !scopeOverride) {
|
|
97
|
+
print('');
|
|
98
|
+
const chosenScope = await scopePrompt('project');
|
|
99
|
+
if (chosenScope === null) {
|
|
100
|
+
print('\nCancelled.');
|
|
101
|
+
process.exit(0);
|
|
102
|
+
}
|
|
103
|
+
scope = chosenScope;
|
|
104
|
+
}
|
|
105
|
+
// Write wrapper marketplace
|
|
106
|
+
print('');
|
|
107
|
+
printStep('Creating wrapper marketplace...');
|
|
108
|
+
const { wrapperDir, marketplaceName } = writeWrapperMarketplace({
|
|
109
|
+
ownerRepo,
|
|
110
|
+
pluginName: detection.name,
|
|
111
|
+
scope,
|
|
112
|
+
strict: detection.strict,
|
|
113
|
+
});
|
|
114
|
+
printSuccess(`Wrapper at ${DIM}${wrapperDir}${RESET}`);
|
|
115
|
+
// Add wrapper marketplace
|
|
116
|
+
printStep('Adding marketplace...');
|
|
117
|
+
const addResult = claudeMarketplaceAdd(wrapperDir, scope);
|
|
118
|
+
if (!addResult.ok) {
|
|
119
|
+
printError(`Failed to add marketplace: ${addResult.output}`);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
printSuccess('Marketplace added');
|
|
123
|
+
// Install the plugin
|
|
124
|
+
printStep(`Installing ${detection.name}...`);
|
|
125
|
+
const installResult = claudePluginInstall(detection.name, marketplaceName, scope);
|
|
126
|
+
if (!installResult.ok) {
|
|
127
|
+
printError(`Failed to install plugin: ${installResult.output}`);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
printSuccess(detection.name);
|
|
131
|
+
print('');
|
|
132
|
+
print(`${GREEN}${BOLD}Done!${RESET} Plugin "${detection.name}" installed.`);
|
|
133
|
+
print('');
|
|
134
|
+
print(`${DIM}Manage with:${RESET}`);
|
|
135
|
+
print(` ${DIM}npx claudepluginhub remove ${ownerRepo} --scope ${scope}${RESET}`);
|
|
136
|
+
print(` ${DIM}claude plugin list${RESET}`);
|
|
137
|
+
print('');
|
|
138
|
+
}
|
|
139
|
+
export async function runGithubRemove(ownerRepo, scopeOverride) {
|
|
140
|
+
let scope;
|
|
141
|
+
if (scopeOverride) {
|
|
142
|
+
scope = scopeOverride;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
const scopes = findWrapperScopes(ownerRepo);
|
|
146
|
+
if (scopes.length === 0) {
|
|
147
|
+
printMarketplaceGuidance(ownerRepo);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (scopes.length === 1) {
|
|
151
|
+
scope = scopes[0];
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
printError(`Found ${ownerRepo} in multiple scopes: ${scopes.join(', ')}. Use --scope to specify.`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const meta = readWrapperMeta(ownerRepo, scope);
|
|
159
|
+
if (!meta) {
|
|
160
|
+
printMarketplaceGuidance(ownerRepo);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
printStep(`Removing ${ownerRepo} (scope: ${scope})...`);
|
|
164
|
+
let anyFailed = false;
|
|
165
|
+
// Best-effort: uninstall plugin
|
|
166
|
+
const uninstallResult = claudePluginUninstall(meta.pluginName, scope);
|
|
167
|
+
if (uninstallResult.ok) {
|
|
168
|
+
printSuccess(`Uninstalled plugin ${meta.pluginName}`);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
printWarning(`Could not uninstall plugin: ${uninstallResult.output}`);
|
|
172
|
+
anyFailed = true;
|
|
173
|
+
}
|
|
174
|
+
// Best-effort: remove marketplace
|
|
175
|
+
const removeResult = claudeMarketplaceRemove(meta.marketplaceName);
|
|
176
|
+
if (removeResult.ok) {
|
|
177
|
+
printSuccess(`Removed marketplace ${meta.marketplaceName}`);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
printWarning(`Could not remove marketplace: ${removeResult.output}`);
|
|
181
|
+
anyFailed = true;
|
|
182
|
+
}
|
|
183
|
+
if (anyFailed) {
|
|
184
|
+
// Keep wrapper metadata so the user can retry cleanup later
|
|
185
|
+
print('');
|
|
186
|
+
print(`${YELLOW}${BOLD}Partially removed.${RESET} Some cleanup steps failed.`);
|
|
187
|
+
const wrapperDir = getWrapperDir(ownerRepo, scope);
|
|
188
|
+
print(`Wrapper kept at ${DIM}${wrapperDir}${RESET} for retry.`);
|
|
189
|
+
print(`Use ${DIM}claude plugin list${RESET} to check remaining state.`);
|
|
190
|
+
print(`Retry with ${DIM}npx claudepluginhub remove ${ownerRepo} --scope ${scope}${RESET}`);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
// All native cleanup succeeded — safe to delete wrapper
|
|
194
|
+
removeWrapperDir(ownerRepo, scope);
|
|
195
|
+
printSuccess('Removed wrapper directory');
|
|
196
|
+
print('');
|
|
197
|
+
print(`${GREEN}${BOLD}Removed${RESET} ${ownerRepo}`);
|
|
198
|
+
}
|
|
199
|
+
print('');
|
|
200
|
+
}
|
|
201
|
+
function printMarketplaceGuidance(ownerRepo) {
|
|
202
|
+
print(`No standalone wrapper found for ${ownerRepo}.`);
|
|
203
|
+
print('');
|
|
204
|
+
print(`If installed as a marketplace, use Claude Code directly:`);
|
|
205
|
+
print(` ${DIM}claude plugin marketplace remove <name>${RESET}`);
|
|
206
|
+
print(` ${DIM}claude plugin uninstall <plugin>${RESET}`);
|
|
207
|
+
}
|
|
208
|
+
function printNativeGuidance() {
|
|
209
|
+
print(`${DIM}Manage with Claude Code:${RESET}`);
|
|
210
|
+
print(` ${DIM}claude plugin list${RESET}`);
|
|
211
|
+
print(` ${DIM}claude plugin uninstall <plugin>${RESET}`);
|
|
212
|
+
print(` ${DIM}claude plugin marketplace remove <name>${RESET}`);
|
|
213
|
+
print('');
|
|
214
|
+
}
|
package/dist/github.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
interface TreeEntry {
|
|
1
|
+
export interface TreeEntry {
|
|
2
2
|
path: string;
|
|
3
3
|
mode: string;
|
|
4
4
|
type: 'blob' | 'tree';
|
|
5
5
|
sha: string;
|
|
6
6
|
size?: number;
|
|
7
7
|
}
|
|
8
|
-
interface TreeResponse {
|
|
8
|
+
export interface TreeResponse {
|
|
9
9
|
sha: string;
|
|
10
10
|
tree: TreeEntry[];
|
|
11
11
|
truncated: boolean;
|
|
@@ -16,4 +16,3 @@ export declare function downloadFile(repo: string, branch: string, path: string)
|
|
|
16
16
|
export declare function findTreeSha(tree: TreeResponse, dirPath: string): string | null;
|
|
17
17
|
export declare function findBlobSha(tree: TreeResponse, filePath: string): string | null;
|
|
18
18
|
export declare function listFilesUnder(tree: TreeResponse, dirPath: string): TreeEntry[];
|
|
19
|
-
export {};
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,8 @@ import { runInstall } from './install.js';
|
|
|
5
5
|
import { runUpdate } from './update.js';
|
|
6
6
|
import { runRemove } from './remove.js';
|
|
7
7
|
import { runList } from './list.js';
|
|
8
|
+
import { isOwnerRepo } from './detect.js';
|
|
9
|
+
import { runGithubInstall, runGithubRemove } from './github-install.js';
|
|
8
10
|
import { print, printBanner, printStep, printSuccess, printError, DIM, RESET, GREEN, CYAN, YELLOW, } from './output.js';
|
|
9
11
|
const VALID_SCOPES = ['user', 'project', 'local'];
|
|
10
12
|
const TYPE_BADGES = {
|
|
@@ -19,16 +21,22 @@ function printUsage() {
|
|
|
19
21
|
print(`Usage: npx claudepluginhub <command> [options]
|
|
20
22
|
|
|
21
23
|
Commands:
|
|
22
|
-
<identifier> Install
|
|
23
|
-
add <identifier> Same as above
|
|
24
|
-
update [identifier] Update
|
|
25
|
-
list List
|
|
26
|
-
remove <identifier> Remove
|
|
24
|
+
<identifier> Install from a source
|
|
25
|
+
add <identifier> Same as above
|
|
26
|
+
update [identifier] Update (u/ collections only)
|
|
27
|
+
list List (u/ collections only)
|
|
28
|
+
remove <identifier> Remove
|
|
27
29
|
|
|
28
30
|
Identifiers:
|
|
29
|
-
|
|
31
|
+
owner/repo GitHub plugin or marketplace (via Claude Code)
|
|
32
|
+
u/<userId>/<slug> ClaudePluginHub collection
|
|
30
33
|
https://claudepluginhub.com/api/user-plugins/.../marketplace.json
|
|
31
34
|
|
|
35
|
+
Note: owner/repo installs use Claude Code's plugin system.
|
|
36
|
+
- list/update only show u/ installs
|
|
37
|
+
- Use \`claude plugin list\` for Claude Code plugins
|
|
38
|
+
- Standalone: \`npx claudepluginhub remove owner/repo\` for cleanup
|
|
39
|
+
|
|
32
40
|
Options:
|
|
33
41
|
--yes, -y Skip prompts (install all, project scope)
|
|
34
42
|
--scope <scope> Set scope: user, project (default), local
|
|
@@ -86,6 +94,11 @@ async function main() {
|
|
|
86
94
|
printError('Missing identifier. Usage: npx claudepluginhub remove <identifier>');
|
|
87
95
|
process.exit(1);
|
|
88
96
|
}
|
|
97
|
+
// owner/repo remove → github wrapper cleanup
|
|
98
|
+
if (isOwnerRepo(identifier)) {
|
|
99
|
+
await runGithubRemove(identifier, scopeOverride);
|
|
100
|
+
process.exit(0);
|
|
101
|
+
}
|
|
89
102
|
await runRemove(identifier);
|
|
90
103
|
process.exit(0);
|
|
91
104
|
}
|
|
@@ -94,12 +107,17 @@ async function main() {
|
|
|
94
107
|
printUsage();
|
|
95
108
|
process.exit(1);
|
|
96
109
|
}
|
|
110
|
+
// owner/repo → native Claude Code install
|
|
111
|
+
if (isOwnerRepo(identifier)) {
|
|
112
|
+
await runGithubInstall(identifier, yes, scopeOverride);
|
|
113
|
+
process.exit(0);
|
|
114
|
+
}
|
|
97
115
|
// Resolve identifier to API URL
|
|
98
116
|
const url = resolveMarketplaceUrl(identifier);
|
|
99
117
|
if (!url) {
|
|
100
118
|
printError(`Invalid identifier: ${identifier}`);
|
|
101
119
|
print('');
|
|
102
|
-
print(`Expected: u/<userId>/<slug
|
|
120
|
+
print(`Expected: owner/repo, u/<userId>/<slug>, or a claudepluginhub.com URL`);
|
|
103
121
|
process.exit(1);
|
|
104
122
|
}
|
|
105
123
|
// Derive the collection identifier from the input
|
package/dist/native.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Scope } from './prompts.js';
|
|
2
|
+
export declare function checkClaudeCli(): boolean;
|
|
3
|
+
export declare function claudeMarketplaceAdd(source: string, scope: Scope): {
|
|
4
|
+
ok: boolean;
|
|
5
|
+
output: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function claudePluginInstall(plugin: string, marketplace: string, scope: Scope): {
|
|
8
|
+
ok: boolean;
|
|
9
|
+
output: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function claudePluginUninstall(plugin: string, scope: Scope): {
|
|
12
|
+
ok: boolean;
|
|
13
|
+
output: string;
|
|
14
|
+
};
|
|
15
|
+
export declare function claudeMarketplaceRemove(name: string): {
|
|
16
|
+
ok: boolean;
|
|
17
|
+
output: string;
|
|
18
|
+
};
|
package/dist/native.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { printError } from './output.js';
|
|
3
|
+
function runClaude(args) {
|
|
4
|
+
try {
|
|
5
|
+
const output = execFileSync('claude', args, {
|
|
6
|
+
encoding: 'utf-8',
|
|
7
|
+
timeout: 60_000,
|
|
8
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
9
|
+
});
|
|
10
|
+
return { ok: true, output: output.trim() };
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
const error = err;
|
|
14
|
+
return { ok: false, output: error.stderr ?? error.message ?? 'Unknown error' };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function checkClaudeCli() {
|
|
18
|
+
const result = runClaude(['plugin', '--help']);
|
|
19
|
+
if (!result.ok) {
|
|
20
|
+
printError('Claude CLI not found or does not support plugins. Install Claude Code first: https://docs.anthropic.com/en/docs/claude-code');
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
export function claudeMarketplaceAdd(source, scope) {
|
|
26
|
+
return runClaude(['plugin', 'marketplace', 'add', source, '--scope', scope]);
|
|
27
|
+
}
|
|
28
|
+
export function claudePluginInstall(plugin, marketplace, scope) {
|
|
29
|
+
return runClaude([
|
|
30
|
+
'plugin',
|
|
31
|
+
'install',
|
|
32
|
+
`${plugin}@${marketplace}`,
|
|
33
|
+
'--scope',
|
|
34
|
+
scope,
|
|
35
|
+
]);
|
|
36
|
+
}
|
|
37
|
+
export function claudePluginUninstall(plugin, scope) {
|
|
38
|
+
return runClaude(['plugin', 'uninstall', plugin, '--scope', scope]);
|
|
39
|
+
}
|
|
40
|
+
export function claudeMarketplaceRemove(name) {
|
|
41
|
+
return runClaude(['plugin', 'marketplace', 'remove', name]);
|
|
42
|
+
}
|
package/dist/output.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export declare const moveUp: (n: number) => string;
|
|
|
13
13
|
export declare function print(message: string): void;
|
|
14
14
|
export declare function printError(message: string): void;
|
|
15
15
|
export declare function printBanner(): void;
|
|
16
|
+
export declare function printWarning(message: string): void;
|
|
16
17
|
export declare function printStep(label: string): void;
|
|
17
18
|
export declare function printSuccess(label: string): void;
|
|
18
19
|
export declare function printFail(label: string, error?: string): void;
|
package/dist/output.js
CHANGED
|
@@ -19,6 +19,9 @@ export function printError(message) {
|
|
|
19
19
|
export function printBanner() {
|
|
20
20
|
print(`\n${CYAN}${BOLD}ClaudePluginHub${RESET} ${DIM}Installer${RESET}\n`);
|
|
21
21
|
}
|
|
22
|
+
export function printWarning(message) {
|
|
23
|
+
console.error(`${YELLOW}Warning:${RESET} ${message}`);
|
|
24
|
+
}
|
|
22
25
|
export function printStep(label) {
|
|
23
26
|
print(`${DIM}>${RESET} ${label}`);
|
|
24
27
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Scope } from './prompts.js';
|
|
2
|
+
export declare function sanitizeName(raw: string): string;
|
|
3
|
+
export declare function validateManifestName(name: string): boolean;
|
|
4
|
+
export declare function getWrapperDir(ownerRepo: string, scope: Scope): string;
|
|
5
|
+
export declare function getMarketplaceName(ownerRepo: string, scope: Scope): string;
|
|
6
|
+
export interface WrapperOptions {
|
|
7
|
+
ownerRepo: string;
|
|
8
|
+
pluginName: string;
|
|
9
|
+
scope: Scope;
|
|
10
|
+
strict: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface WrapperMeta {
|
|
13
|
+
ownerRepo: string;
|
|
14
|
+
pluginName: string;
|
|
15
|
+
marketplaceName: string;
|
|
16
|
+
scope: Scope;
|
|
17
|
+
cwd: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function writeWrapperMarketplace(opts: WrapperOptions): {
|
|
20
|
+
wrapperDir: string;
|
|
21
|
+
marketplaceName: string;
|
|
22
|
+
};
|
|
23
|
+
export declare function readWrapperMeta(ownerRepo: string, scope: Scope): WrapperMeta | null;
|
|
24
|
+
export declare function findWrapperScopes(ownerRepo: string): Scope[];
|
|
25
|
+
export declare function removeWrapperDir(ownerRepo: string, scope: Scope): void;
|
package/dist/wrapper.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
const VALID_NAME_RE = /^[a-zA-Z0-9_.-]+$/;
|
|
6
|
+
export function sanitizeName(raw) {
|
|
7
|
+
return (raw
|
|
8
|
+
.toLowerCase()
|
|
9
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
10
|
+
.replace(/-+/g, '-')
|
|
11
|
+
.replace(/^-|-$/g, '') || 'plugin');
|
|
12
|
+
}
|
|
13
|
+
export function validateManifestName(name) {
|
|
14
|
+
return VALID_NAME_RE.test(name);
|
|
15
|
+
}
|
|
16
|
+
function cwdHash() {
|
|
17
|
+
return createHash('sha256').update(process.cwd()).digest('hex').slice(0, 8);
|
|
18
|
+
}
|
|
19
|
+
function wrapperBaseName(ownerRepo, scope) {
|
|
20
|
+
const [owner, repo] = ownerRepo.split('/');
|
|
21
|
+
if (scope === 'local') {
|
|
22
|
+
return `${owner}-${repo}-local-${cwdHash()}`;
|
|
23
|
+
}
|
|
24
|
+
return `${owner}-${repo}-${scope}`;
|
|
25
|
+
}
|
|
26
|
+
export function getWrapperDir(ownerRepo, scope) {
|
|
27
|
+
const name = wrapperBaseName(ownerRepo, scope);
|
|
28
|
+
if (scope === 'project') {
|
|
29
|
+
return join(process.cwd(), '.claude', '.cpd-wrappers', name);
|
|
30
|
+
}
|
|
31
|
+
// user and local both go under ~/.claude/
|
|
32
|
+
return join(homedir(), '.claude', '.cpd-wrappers', name);
|
|
33
|
+
}
|
|
34
|
+
export function getMarketplaceName(ownerRepo, scope) {
|
|
35
|
+
const [owner, repo] = ownerRepo.split('/');
|
|
36
|
+
if (scope === 'local') {
|
|
37
|
+
return `cpd-${sanitizeName(owner)}-${sanitizeName(repo)}-local-${cwdHash()}`;
|
|
38
|
+
}
|
|
39
|
+
return `cpd-${sanitizeName(owner)}-${sanitizeName(repo)}-${scope}`;
|
|
40
|
+
}
|
|
41
|
+
export function writeWrapperMarketplace(opts) {
|
|
42
|
+
const { ownerRepo, pluginName, scope, strict } = opts;
|
|
43
|
+
const wrapperDir = getWrapperDir(ownerRepo, scope);
|
|
44
|
+
const marketplaceName = getMarketplaceName(ownerRepo, scope);
|
|
45
|
+
const marketplace = {
|
|
46
|
+
name: marketplaceName,
|
|
47
|
+
owner: { name: 'ClaudePluginHub CLI' },
|
|
48
|
+
plugins: [
|
|
49
|
+
{
|
|
50
|
+
name: pluginName,
|
|
51
|
+
source: { source: 'github', repo: ownerRepo },
|
|
52
|
+
strict,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
const meta = {
|
|
57
|
+
ownerRepo,
|
|
58
|
+
pluginName,
|
|
59
|
+
marketplaceName,
|
|
60
|
+
scope,
|
|
61
|
+
cwd: process.cwd(),
|
|
62
|
+
};
|
|
63
|
+
mkdirSync(join(wrapperDir, '.claude-plugin'), { recursive: true });
|
|
64
|
+
writeFileSync(join(wrapperDir, '.claude-plugin', 'marketplace.json'), JSON.stringify(marketplace, null, 2) + '\n');
|
|
65
|
+
writeFileSync(join(wrapperDir, 'cpd-meta.json'), JSON.stringify(meta, null, 2) + '\n');
|
|
66
|
+
return { wrapperDir, marketplaceName };
|
|
67
|
+
}
|
|
68
|
+
export function readWrapperMeta(ownerRepo, scope) {
|
|
69
|
+
const dir = getWrapperDir(ownerRepo, scope);
|
|
70
|
+
const metaPath = join(dir, 'cpd-meta.json');
|
|
71
|
+
if (!existsSync(metaPath))
|
|
72
|
+
return null;
|
|
73
|
+
try {
|
|
74
|
+
return JSON.parse(readFileSync(metaPath, 'utf-8'));
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export function findWrapperScopes(ownerRepo) {
|
|
81
|
+
const scopes = [];
|
|
82
|
+
const [owner, repo] = ownerRepo.split('/');
|
|
83
|
+
// Check user scope
|
|
84
|
+
const userDir = join(homedir(), '.claude', '.cpd-wrappers');
|
|
85
|
+
if (existsSync(userDir)) {
|
|
86
|
+
const userWrapper = join(userDir, `${owner}-${repo}-user`);
|
|
87
|
+
if (existsSync(join(userWrapper, 'cpd-meta.json'))) {
|
|
88
|
+
scopes.push('user');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Check project scope
|
|
92
|
+
const projectDir = join(process.cwd(), '.claude', '.cpd-wrappers');
|
|
93
|
+
if (existsSync(projectDir)) {
|
|
94
|
+
const projectWrapper = join(projectDir, `${owner}-${repo}-project`);
|
|
95
|
+
if (existsSync(join(projectWrapper, 'cpd-meta.json'))) {
|
|
96
|
+
scopes.push('project');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Check local scope — only matches current cwd
|
|
100
|
+
if (existsSync(userDir)) {
|
|
101
|
+
const localWrapper = join(userDir, `${owner}-${repo}-local-${cwdHash()}`);
|
|
102
|
+
if (existsSync(join(localWrapper, 'cpd-meta.json'))) {
|
|
103
|
+
scopes.push('local');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return scopes;
|
|
107
|
+
}
|
|
108
|
+
export function removeWrapperDir(ownerRepo, scope) {
|
|
109
|
+
const dir = getWrapperDir(ownerRepo, scope);
|
|
110
|
+
if (existsSync(dir)) {
|
|
111
|
+
rmSync(dir, { recursive: true, force: true });
|
|
112
|
+
}
|
|
113
|
+
}
|