claudepluginhub 0.4.2 → 0.5.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/README.md +8 -0
- package/dist/args.d.ts +10 -0
- package/dist/args.js +62 -0
- package/dist/detect.d.ts +2 -1
- package/dist/detect.js +5 -13
- package/dist/github-install.d.ts +1 -1
- package/dist/github-install.js +18 -5
- package/dist/index.js +6 -31
- package/dist/validation.d.ts +7 -0
- package/dist/validation.js +15 -0
- package/dist/wrapper.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,10 +42,18 @@ npx claudepluginhub list List (u/ collections only)
|
|
|
42
42
|
npx claudepluginhub remove <identifier> Remove
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
## Examples
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Install a specific plugin from a marketplace repo
|
|
49
|
+
npx claudepluginhub vercel/next.js --plugin cache-components
|
|
50
|
+
```
|
|
51
|
+
|
|
45
52
|
## Options
|
|
46
53
|
|
|
47
54
|
| Option | Description |
|
|
48
55
|
|--------|-------------|
|
|
56
|
+
| `--plugin <name>` | Install a specific plugin from a marketplace repo |
|
|
49
57
|
| `--yes, -y` | Skip prompts (install all, project scope) |
|
|
50
58
|
| `--scope <scope>` | Set scope: `user`, `project` (default), `local` |
|
|
51
59
|
| `-h, --help` | Show help |
|
package/dist/args.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Scope } from './prompts.js';
|
|
2
|
+
export interface ParsedArgs {
|
|
3
|
+
command: string;
|
|
4
|
+
identifier: string | null;
|
|
5
|
+
plugin: string | null;
|
|
6
|
+
yes: boolean;
|
|
7
|
+
scope: Scope | null;
|
|
8
|
+
}
|
|
9
|
+
export declare function parseArgs(argv: string[]): ParsedArgs;
|
|
10
|
+
export declare function validatePlugin(args: ParsedArgs): void;
|
package/dist/args.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { VALID_NAME_RE, isOwnerRepo } from './validation.js';
|
|
2
|
+
import { printError } from './output.js';
|
|
3
|
+
const VALID_SCOPES = ['user', 'project', 'local'];
|
|
4
|
+
export function parseArgs(argv) {
|
|
5
|
+
const yes = argv.includes('--yes') || argv.includes('-y');
|
|
6
|
+
let scope = null;
|
|
7
|
+
let plugin = null;
|
|
8
|
+
const scopeIdx = argv.indexOf('--scope');
|
|
9
|
+
if (scopeIdx !== -1) {
|
|
10
|
+
const val = argv[scopeIdx + 1];
|
|
11
|
+
if (!val || val.startsWith('-')) {
|
|
12
|
+
printError('Missing value for --scope. Must be: user, project, or local');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
if (!VALID_SCOPES.includes(val)) {
|
|
16
|
+
printError(`Invalid scope "${val}". Must be: user, project, or local`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
scope = val;
|
|
20
|
+
}
|
|
21
|
+
const pluginIdx = argv.indexOf('--plugin');
|
|
22
|
+
if (pluginIdx !== -1) {
|
|
23
|
+
const val = argv[pluginIdx + 1];
|
|
24
|
+
if (!val || val.startsWith('-')) {
|
|
25
|
+
printError('Missing value for --plugin');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
if (!VALID_NAME_RE.test(val)) {
|
|
29
|
+
printError(`Invalid plugin name "${val}". Must match [a-zA-Z0-9_.-]+`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
plugin = val;
|
|
33
|
+
}
|
|
34
|
+
// Filter out flags and their values
|
|
35
|
+
const positional = argv.filter((a, i) => !a.startsWith('-') &&
|
|
36
|
+
(i === 0 || (argv[i - 1] !== '--scope' && argv[i - 1] !== '--plugin')));
|
|
37
|
+
const first = positional[0] ?? null;
|
|
38
|
+
const second = positional[1] ?? null;
|
|
39
|
+
// Route commands
|
|
40
|
+
if (first === 'list')
|
|
41
|
+
return { command: 'list', identifier: null, plugin, yes, scope };
|
|
42
|
+
if (first === 'update')
|
|
43
|
+
return { command: 'update', identifier: second, plugin, yes, scope };
|
|
44
|
+
if (first === 'remove')
|
|
45
|
+
return { command: 'remove', identifier: second, plugin, yes, scope };
|
|
46
|
+
if (first === 'add')
|
|
47
|
+
return { command: 'add', identifier: second, plugin, yes, scope };
|
|
48
|
+
// Default: treat first positional as identifier for install
|
|
49
|
+
return { command: 'add', identifier: first, plugin, yes, scope };
|
|
50
|
+
}
|
|
51
|
+
export function validatePlugin(args) {
|
|
52
|
+
if (!args.plugin)
|
|
53
|
+
return;
|
|
54
|
+
if (args.command !== 'add') {
|
|
55
|
+
printError('--plugin is only valid for install commands');
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
if (!args.identifier || !isOwnerRepo(args.identifier)) {
|
|
59
|
+
printError('--plugin is only valid with owner/repo identifiers');
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
}
|
package/dist/detect.d.ts
CHANGED
package/dist/detect.js
CHANGED
|
@@ -1,17 +1,8 @@
|
|
|
1
1
|
import { fetchDefaultBranch, fetchTree, downloadFile } from './github.js';
|
|
2
2
|
import { printError } from './output.js';
|
|
3
|
+
import { VALID_NAME_RE, isOwnerRepo } from './validation.js';
|
|
3
4
|
import { sanitizeName } from './wrapper.js';
|
|
4
|
-
|
|
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
|
-
}
|
|
5
|
+
export { isOwnerRepo };
|
|
15
6
|
const COMPONENT_PATTERNS = [
|
|
16
7
|
/^commands\/.*\.md$/,
|
|
17
8
|
/^agents\/.*\.md$/,
|
|
@@ -95,7 +86,8 @@ export async function detectRepo(ownerRepo) {
|
|
|
95
86
|
const source = rawSource &&
|
|
96
87
|
typeof rawSource === 'object' &&
|
|
97
88
|
typeof rawSource.source === 'string' &&
|
|
98
|
-
typeof rawSource.repo === 'string'
|
|
89
|
+
typeof rawSource.repo === 'string' &&
|
|
90
|
+
isOwnerRepo(rawSource.repo)
|
|
99
91
|
? rawSource
|
|
100
92
|
: { source: 'github', repo: ownerRepo };
|
|
101
93
|
validPlugins.push({
|
|
@@ -136,7 +128,7 @@ export async function detectRepo(ownerRepo) {
|
|
|
136
128
|
printError(`plugin.json has an invalid name: "${pluginJson.name}". Must match [a-zA-Z0-9_.-]+`);
|
|
137
129
|
process.exit(1);
|
|
138
130
|
}
|
|
139
|
-
const strict = pluginJson.strict
|
|
131
|
+
const strict = pluginJson.strict === false ? false : true;
|
|
140
132
|
return { kind: 'plugin', name: pluginJson.name, strict };
|
|
141
133
|
}
|
|
142
134
|
// Plugin without manifest — check for component files at root
|
package/dist/github-install.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { Scope } from './prompts.js';
|
|
2
|
-
export declare function runGithubInstall(ownerRepo: string, yes: boolean, scopeOverride: Scope | null): Promise<void>;
|
|
2
|
+
export declare function runGithubInstall(ownerRepo: string, yes: boolean, scopeOverride: Scope | null, pluginName?: string | null): Promise<void>;
|
|
3
3
|
export declare function runGithubRemove(ownerRepo: string, scopeOverride: Scope | null): Promise<void>;
|
package/dist/github-install.js
CHANGED
|
@@ -3,7 +3,7 @@ import { detectRepo } from './detect.js';
|
|
|
3
3
|
import { checkClaudeCli, claudeMarketplaceAdd, claudePluginInstall, claudePluginUninstall, claudeMarketplaceRemove, } from './native.js';
|
|
4
4
|
import { writeWrapperMarketplace, readWrapperMeta, findWrapperScopes, removeWrapperDir, getWrapperDir, } from './wrapper.js';
|
|
5
5
|
import { print, printStep, printSuccess, printFail, printError, printWarning, BOLD, RESET, DIM, GREEN, YELLOW, } from './output.js';
|
|
6
|
-
export async function runGithubInstall(ownerRepo, yes, scopeOverride) {
|
|
6
|
+
export async function runGithubInstall(ownerRepo, yes, scopeOverride, pluginName = null) {
|
|
7
7
|
if (!checkClaudeCli())
|
|
8
8
|
process.exit(1);
|
|
9
9
|
printStep('Detecting repository type...');
|
|
@@ -12,23 +12,36 @@ export async function runGithubInstall(ownerRepo, yes, scopeOverride) {
|
|
|
12
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
13
|
process.exit(1);
|
|
14
14
|
}
|
|
15
|
+
if (pluginName && detection.kind === 'plugin') {
|
|
16
|
+
printError(`--plugin is only valid for marketplace repos. "${ownerRepo}" is a standalone plugin.`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
15
19
|
if (detection.kind === 'marketplace') {
|
|
16
|
-
await installMarketplace(ownerRepo, detection, yes, scopeOverride);
|
|
20
|
+
await installMarketplace(ownerRepo, detection, yes, scopeOverride, pluginName);
|
|
17
21
|
}
|
|
18
22
|
else {
|
|
19
23
|
await installStandalone(ownerRepo, detection, yes, scopeOverride);
|
|
20
24
|
}
|
|
21
25
|
}
|
|
22
|
-
async function installMarketplace(ownerRepo, detection, yes, scopeOverride) {
|
|
26
|
+
async function installMarketplace(ownerRepo, detection, yes, scopeOverride, pluginName = null) {
|
|
23
27
|
const { manifest } = detection;
|
|
24
28
|
printSuccess(`Marketplace "${manifest.name}" — ${manifest.plugins.length} plugin(s)`);
|
|
25
29
|
if (manifest.plugins.length === 0) {
|
|
26
30
|
printError('No valid plugins found in marketplace.');
|
|
27
31
|
process.exit(1);
|
|
28
32
|
}
|
|
29
|
-
// Plugin
|
|
33
|
+
// Plugin selection
|
|
30
34
|
let selectedPlugins = manifest.plugins;
|
|
31
|
-
if (
|
|
35
|
+
if (pluginName) {
|
|
36
|
+
const match = manifest.plugins.find((p) => p.name === pluginName);
|
|
37
|
+
if (!match) {
|
|
38
|
+
printError(`Plugin "${pluginName}" not found in marketplace "${manifest.name}". ` +
|
|
39
|
+
`Available: ${manifest.plugins.map((p) => p.name).join(', ')}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
selectedPlugins = [match];
|
|
43
|
+
}
|
|
44
|
+
else if (!yes && manifest.plugins.length > 1) {
|
|
32
45
|
print('');
|
|
33
46
|
const items = manifest.plugins.map((p) => ({
|
|
34
47
|
label: p.name,
|
package/dist/index.js
CHANGED
|
@@ -7,8 +7,8 @@ import { runRemove } from './remove.js';
|
|
|
7
7
|
import { runList } from './list.js';
|
|
8
8
|
import { isOwnerRepo } from './detect.js';
|
|
9
9
|
import { runGithubInstall, runGithubRemove } from './github-install.js';
|
|
10
|
+
import { parseArgs, validatePlugin } from './args.js';
|
|
10
11
|
import { print, printBanner, printStep, printSuccess, printError, DIM, RESET, GREEN, CYAN, YELLOW, } from './output.js';
|
|
11
|
-
const VALID_SCOPES = ['user', 'project', 'local'];
|
|
12
12
|
const TYPE_BADGES = {
|
|
13
13
|
skill: `${GREEN}[skill]${RESET}`,
|
|
14
14
|
command: `${CYAN}[cmd]${RESET}`,
|
|
@@ -38,39 +38,12 @@ Note: owner/repo installs use Claude Code's plugin system.
|
|
|
38
38
|
- Standalone: \`npx claudepluginhub remove owner/repo\` for cleanup
|
|
39
39
|
|
|
40
40
|
Options:
|
|
41
|
+
--plugin <name> Install a specific plugin from a marketplace repo
|
|
41
42
|
--yes, -y Skip prompts (install all, project scope)
|
|
42
43
|
--scope <scope> Set scope: user, project (default), local
|
|
43
44
|
-h, --help Show this help
|
|
44
45
|
`);
|
|
45
46
|
}
|
|
46
|
-
function parseArgs(argv) {
|
|
47
|
-
const yes = argv.includes('--yes') || argv.includes('-y');
|
|
48
|
-
let scope = null;
|
|
49
|
-
const scopeIdx = argv.indexOf('--scope');
|
|
50
|
-
if (scopeIdx !== -1 && argv[scopeIdx + 1]) {
|
|
51
|
-
const val = argv[scopeIdx + 1];
|
|
52
|
-
if (!VALID_SCOPES.includes(val)) {
|
|
53
|
-
printError(`Invalid scope "${val}". Must be: user, project, or local`);
|
|
54
|
-
process.exit(1);
|
|
55
|
-
}
|
|
56
|
-
scope = val;
|
|
57
|
-
}
|
|
58
|
-
// Filter out flags and their values
|
|
59
|
-
const positional = argv.filter((a, i) => !a.startsWith('-') && (i === 0 || argv[i - 1] !== '--scope'));
|
|
60
|
-
const first = positional[0] ?? null;
|
|
61
|
-
const second = positional[1] ?? null;
|
|
62
|
-
// Route commands
|
|
63
|
-
if (first === 'list')
|
|
64
|
-
return { command: 'list', identifier: null, yes, scope };
|
|
65
|
-
if (first === 'update')
|
|
66
|
-
return { command: 'update', identifier: second, yes, scope };
|
|
67
|
-
if (first === 'remove')
|
|
68
|
-
return { command: 'remove', identifier: second, yes, scope };
|
|
69
|
-
if (first === 'add')
|
|
70
|
-
return { command: 'add', identifier: second, yes, scope };
|
|
71
|
-
// Default: treat first positional as identifier for install
|
|
72
|
-
return { command: 'add', identifier: first, yes, scope };
|
|
73
|
-
}
|
|
74
47
|
async function main() {
|
|
75
48
|
const args = process.argv.slice(2);
|
|
76
49
|
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
|
@@ -78,7 +51,9 @@ async function main() {
|
|
|
78
51
|
printUsage();
|
|
79
52
|
process.exit(0);
|
|
80
53
|
}
|
|
81
|
-
const
|
|
54
|
+
const parsed = parseArgs(args);
|
|
55
|
+
validatePlugin(parsed);
|
|
56
|
+
const { command, identifier, plugin, yes, scope: scopeOverride } = parsed;
|
|
82
57
|
printBanner();
|
|
83
58
|
// Handle non-install commands
|
|
84
59
|
if (command === 'list') {
|
|
@@ -109,7 +84,7 @@ async function main() {
|
|
|
109
84
|
}
|
|
110
85
|
// owner/repo → native Claude Code install
|
|
111
86
|
if (isOwnerRepo(identifier)) {
|
|
112
|
-
await runGithubInstall(identifier, yes, scopeOverride);
|
|
87
|
+
await runGithubInstall(identifier, yes, scopeOverride, plugin);
|
|
113
88
|
process.exit(0);
|
|
114
89
|
}
|
|
115
90
|
// Resolve identifier to API URL
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Shared validation constants and helpers for the CLI. */
|
|
2
|
+
/** Matches a single valid name segment: alphanumeric, underscore, dot, hyphen. */
|
|
3
|
+
export declare const VALID_NAME_RE: RegExp;
|
|
4
|
+
/** Matches an owner/repo string (two valid name segments separated by /). */
|
|
5
|
+
export declare const OWNER_REPO_RE: RegExp;
|
|
6
|
+
/** Validates that a string is a well-formed owner/repo identifier. */
|
|
7
|
+
export declare function isOwnerRepo(identifier: string): boolean;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Shared validation constants and helpers for the CLI. */
|
|
2
|
+
/** Matches a single valid name segment: alphanumeric, underscore, dot, hyphen. */
|
|
3
|
+
export const VALID_NAME_RE = /^[a-zA-Z0-9_.-]+$/;
|
|
4
|
+
/** Matches an owner/repo string (two valid name segments separated by /). */
|
|
5
|
+
export const OWNER_REPO_RE = /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/;
|
|
6
|
+
/** Validates that a string is a well-formed owner/repo identifier. */
|
|
7
|
+
export function isOwnerRepo(identifier) {
|
|
8
|
+
if (!OWNER_REPO_RE.test(identifier))
|
|
9
|
+
return false;
|
|
10
|
+
const [owner, repo] = identifier.split('/');
|
|
11
|
+
// Reject segments that are purely dots (e.g. "..", ".")
|
|
12
|
+
if (/^\.+$/.test(owner) || /^\.+$/.test(repo))
|
|
13
|
+
return false;
|
|
14
|
+
return true;
|
|
15
|
+
}
|
package/dist/wrapper.js
CHANGED
|
@@ -2,7 +2,7 @@ import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from 'node
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
|
-
|
|
5
|
+
import { VALID_NAME_RE } from './validation.js';
|
|
6
6
|
export function sanitizeName(raw) {
|
|
7
7
|
return (raw
|
|
8
8
|
.toLowerCase()
|