claudepluginhub 0.3.2 → 0.4.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 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
- This will:
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
- 1. Fetch the plugin's marketplace JSON
14
- 2. Register the marketplace with Claude Code
15
- 3. Install all plugins from the marketplace
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
- You can also pass the full marketplace JSON URL:
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)
@@ -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,144 @@
1
+ import { fetchDefaultBranch, fetchTree, downloadFile } from './github.js';
2
+ import { printError } from './output.js';
3
+ const OWNER_REPO_RE = /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/;
4
+ const VALID_NAME_RE = /^[a-zA-Z0-9_.-]+$/;
5
+ export function isOwnerRepo(identifier) {
6
+ return OWNER_REPO_RE.test(identifier);
7
+ }
8
+ const COMPONENT_PATTERNS = [
9
+ /^commands\/.*\.md$/,
10
+ /^agents\/.*\.md$/,
11
+ /^skills\/.*\/SKILL\.md$/,
12
+ /^hooks\/hooks\.json$/,
13
+ /^\.mcp\.json$/,
14
+ /^\.lsp\.json$/,
15
+ ];
16
+ export function hasComponentFiles(tree, prefix) {
17
+ for (const entry of tree) {
18
+ if (entry.type !== 'blob')
19
+ continue;
20
+ const path = prefix ? entry.path.slice(prefix.length) : entry.path;
21
+ if (COMPONENT_PATTERNS.some((p) => p.test(path)))
22
+ return true;
23
+ }
24
+ return false;
25
+ }
26
+ export async function detectRepo(ownerRepo) {
27
+ const branch = await fetchDefaultBranch(ownerRepo);
28
+ if (!branch) {
29
+ printError(`Could not access repository ${ownerRepo}`);
30
+ process.exit(1);
31
+ }
32
+ const tree = await fetchTree(ownerRepo, branch);
33
+ if (!tree) {
34
+ printError(`Could not fetch file tree for ${ownerRepo}`);
35
+ process.exit(1);
36
+ }
37
+ if (tree.truncated) {
38
+ printError(`Repository tree is too large (truncated). Cannot detect plugin structure.`);
39
+ process.exit(1);
40
+ }
41
+ const hasMarketplace = tree.tree.some((e) => e.type === 'blob' && e.path === '.claude-plugin/marketplace.json');
42
+ const hasPluginJson = tree.tree.some((e) => e.type === 'blob' && e.path === '.claude-plugin/plugin.json');
43
+ // Marketplace repo
44
+ if (hasMarketplace) {
45
+ const content = await downloadFile(ownerRepo, branch, '.claude-plugin/marketplace.json');
46
+ if (!content) {
47
+ printError('Failed to download marketplace.json');
48
+ process.exit(1);
49
+ }
50
+ let manifest;
51
+ try {
52
+ manifest = JSON.parse(content);
53
+ }
54
+ catch {
55
+ printError('marketplace.json is not valid JSON');
56
+ process.exit(1);
57
+ }
58
+ if (!manifest.name || typeof manifest.name !== 'string') {
59
+ printError('marketplace.json is missing the required "name" field');
60
+ process.exit(1);
61
+ }
62
+ const rawPlugins = manifest.plugins;
63
+ if (!Array.isArray(rawPlugins)) {
64
+ printError('marketplace.json "plugins" must be an array');
65
+ process.exit(1);
66
+ }
67
+ const validPlugins = [];
68
+ for (const p of rawPlugins) {
69
+ if (!p || typeof p !== 'object') {
70
+ printError('marketplace.json contains a non-object plugin entry');
71
+ process.exit(1);
72
+ }
73
+ const plugin = p;
74
+ const name = plugin.name;
75
+ if (typeof name !== 'string' || name.length === 0) {
76
+ printError(`marketplace.json plugin is missing the required "name" field`);
77
+ process.exit(1);
78
+ }
79
+ if (!VALID_NAME_RE.test(name)) {
80
+ printError(`marketplace.json plugin has an invalid name: "${name}". Must match [a-zA-Z0-9_.-]+`);
81
+ process.exit(1);
82
+ }
83
+ validPlugins.push({
84
+ name,
85
+ description: typeof plugin.description === 'string' ? plugin.description : null,
86
+ source: plugin.source ?? {
87
+ source: 'github',
88
+ repo: ownerRepo,
89
+ },
90
+ strict: plugin.strict ?? true,
91
+ });
92
+ }
93
+ return {
94
+ kind: 'marketplace',
95
+ manifest: {
96
+ name: manifest.name,
97
+ plugins: validPlugins,
98
+ },
99
+ };
100
+ }
101
+ // Plugin with manifest
102
+ if (hasPluginJson) {
103
+ const content = await downloadFile(ownerRepo, branch, '.claude-plugin/plugin.json');
104
+ if (!content) {
105
+ printError('Failed to download plugin.json');
106
+ process.exit(1);
107
+ }
108
+ let pluginJson;
109
+ try {
110
+ pluginJson = JSON.parse(content);
111
+ }
112
+ catch {
113
+ printError('plugin.json is not valid JSON');
114
+ process.exit(1);
115
+ }
116
+ if (!('name' in pluginJson) || typeof pluginJson.name !== 'string') {
117
+ printError('plugin.json is missing the required "name" field');
118
+ process.exit(1);
119
+ }
120
+ if (!VALID_NAME_RE.test(pluginJson.name)) {
121
+ printError(`plugin.json has an invalid name: "${pluginJson.name}". Must match [a-zA-Z0-9_.-]+`);
122
+ process.exit(1);
123
+ }
124
+ const strict = pluginJson.strict !== false;
125
+ return { kind: 'plugin', name: pluginJson.name, strict };
126
+ }
127
+ // Plugin without manifest — check for component files at root
128
+ if (hasComponentFiles(tree.tree)) {
129
+ const repo = ownerRepo.split('/')[1];
130
+ return {
131
+ kind: 'plugin',
132
+ name: sanitizeName(repo),
133
+ strict: false,
134
+ };
135
+ }
136
+ return { kind: 'unknown' };
137
+ }
138
+ function sanitizeName(raw) {
139
+ return (raw
140
+ .toLowerCase()
141
+ .replace(/[^a-z0-9-]/g, '-')
142
+ .replace(/-+/g, '-')
143
+ .replace(/^-|-$/g, '') || 'plugin');
144
+ }
@@ -0,0 +1,3 @@
1
+ import type { Scope } from './prompts.js';
2
+ export declare function runGithubInstall(ownerRepo: string, yes: boolean, scopeOverride: Scope | null): Promise<void>;
3
+ export declare function runGithubRemove(ownerRepo: string, scopeOverride: Scope | null): Promise<void>;
@@ -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 components from a user-plugin collection
23
- add <identifier> Same as above (explicit)
24
- update [identifier] Update installed components
25
- list List installed components
26
- remove <identifier> Remove installed components
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
- u/<userId>/<slug> Short form
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> or a claudepluginhub.com URL`);
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
@@ -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;
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudepluginhub",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "Install Claude Code components from ClaudePluginHub",
5
5
  "bin": {
6
6
  "claudepluginhub": "dist/index.js"