claudepluginhub 0.6.0 → 0.7.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/dist/args.d.ts CHANGED
@@ -1,10 +1,19 @@
1
1
  import type { Scope } from './prompts.js';
2
+ declare const COMPONENT_FLAGS: readonly ["--skill", "--command", "--agent"];
3
+ export type ComponentType = (typeof COMPONENT_FLAGS)[number] extends `--${infer T}` ? T : never;
2
4
  export interface ParsedArgs {
3
5
  command: string;
4
6
  identifier: string | null;
5
7
  plugin: string | null;
6
8
  yes: boolean;
7
9
  scope: Scope | null;
10
+ componentFlag: {
11
+ type: ComponentType;
12
+ repoPath: string;
13
+ pluginPath: string;
14
+ } | null;
8
15
  }
9
16
  export declare function parseArgs(argv: string[]): ParsedArgs;
10
17
  export declare function validatePlugin(args: ParsedArgs): void;
18
+ export declare function validateComponentFlag(args: ParsedArgs): void;
19
+ export {};
package/dist/args.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import { VALID_NAME_RE, isOwnerRepo } from './validation.js';
2
2
  import { printError } from './output.js';
3
3
  const VALID_SCOPES = ['user', 'project', 'local'];
4
+ const COMPONENT_FLAGS = ['--skill', '--command', '--agent'];
4
5
  export function parseArgs(argv) {
5
6
  const yes = argv.includes('--yes') || argv.includes('-y');
6
7
  let scope = null;
7
8
  let plugin = null;
9
+ let componentFlag = null;
8
10
  const scopeIdx = argv.indexOf('--scope');
9
11
  if (scopeIdx !== -1) {
10
12
  const val = argv[scopeIdx + 1];
@@ -31,22 +33,66 @@ export function parseArgs(argv) {
31
33
  }
32
34
  plugin = val;
33
35
  }
36
+ // Parse component flags (--skill, --command, --agent)
37
+ const foundComponentFlags = [];
38
+ for (const flag of COMPONENT_FLAGS) {
39
+ const idx = argv.indexOf(flag);
40
+ if (idx !== -1) {
41
+ foundComponentFlags.push({ flag, type: flag.slice(2), idx });
42
+ }
43
+ }
44
+ if (foundComponentFlags.length > 1) {
45
+ printError('Only one of --skill, --command, or --agent can be specified');
46
+ process.exit(1);
47
+ }
48
+ if (foundComponentFlags.length === 1) {
49
+ const { type, idx } = foundComponentFlags[0];
50
+ const val = argv[idx + 1];
51
+ if (!val || val.startsWith('-')) {
52
+ printError(`Missing value for --${type}`);
53
+ process.exit(1);
54
+ }
55
+ if (plugin) {
56
+ printError('--plugin and component flags (--skill, --command, --agent) cannot be used together');
57
+ process.exit(1);
58
+ }
59
+ // Parse optional --source-path
60
+ let pluginPath = val;
61
+ const sourcePathIdx = argv.indexOf('--source-path');
62
+ if (sourcePathIdx !== -1) {
63
+ const spVal = argv[sourcePathIdx + 1];
64
+ if (!spVal || spVal.startsWith('-')) {
65
+ printError('Missing value for --source-path');
66
+ process.exit(1);
67
+ }
68
+ pluginPath = spVal;
69
+ }
70
+ componentFlag = { type, repoPath: val, pluginPath };
71
+ }
72
+ // --source-path without a component flag
73
+ if (!componentFlag && argv.includes('--source-path')) {
74
+ printError('--source-path requires a component flag (--skill, --command, or --agent)');
75
+ process.exit(1);
76
+ }
34
77
  // Filter out flags and their values
78
+ const flagsWithValues = new Set(['--scope', '--plugin', '--source-path']);
79
+ for (const f of COMPONENT_FLAGS)
80
+ flagsWithValues.add(f);
35
81
  const positional = argv.filter((a, i) => !a.startsWith('-') &&
36
- (i === 0 || (argv[i - 1] !== '--scope' && argv[i - 1] !== '--plugin')));
82
+ (i === 0 || !flagsWithValues.has(argv[i - 1])));
37
83
  const first = positional[0] ?? null;
38
84
  const second = positional[1] ?? null;
39
85
  // Route commands
40
86
  if (first === 'list')
41
- return { command: 'list', identifier: null, plugin, yes, scope };
87
+ return { command: 'list', identifier: null, plugin, yes, scope, componentFlag };
42
88
  if (first === 'update')
43
- return { command: 'update', identifier: second, plugin, yes, scope };
89
+ return { command: 'update', identifier: second, plugin, yes, scope, componentFlag };
44
90
  if (first === 'remove')
45
- return { command: 'remove', identifier: second, plugin, yes, scope };
91
+ return { command: 'remove', identifier: second, plugin, yes, scope, componentFlag };
46
92
  if (first === 'add')
47
- return { command: 'add', identifier: second, plugin, yes, scope };
93
+ return { command: 'add', identifier: second, plugin, yes, scope, componentFlag };
48
94
  // Default: treat first positional as identifier for install
49
- return { command: 'add', identifier: first, plugin, yes, scope };
95
+ return { command: 'add', identifier: first, plugin, yes, scope, componentFlag };
50
96
  }
51
97
  export function validatePlugin(args) {
52
98
  if (!args.plugin)
@@ -60,3 +106,15 @@ export function validatePlugin(args) {
60
106
  process.exit(1);
61
107
  }
62
108
  }
109
+ export function validateComponentFlag(args) {
110
+ if (!args.componentFlag)
111
+ return;
112
+ if (args.command !== 'add') {
113
+ printError(`--${args.componentFlag.type} is only valid for install commands`);
114
+ process.exit(1);
115
+ }
116
+ if (!args.identifier || !isOwnerRepo(args.identifier)) {
117
+ printError(`--${args.componentFlag.type} is only valid with owner/repo identifiers`);
118
+ process.exit(1);
119
+ }
120
+ }
@@ -0,0 +1,3 @@
1
+ import type { Scope } from './prompts.js';
2
+ import type { ComponentType } from './args.js';
3
+ export declare function runDirectComponentInstall(ownerRepo: string, componentType: ComponentType, repoPath: string, pluginPath: string, yes: boolean, scopeOverride: Scope | null): Promise<void>;
@@ -0,0 +1,186 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { scopePrompt } from './prompts.js';
4
+ import { installComponent, getBaseDir } from './download.js';
5
+ import { readLock, writeLock, addCollection, componentKey } from './lock.js';
6
+ import { fetchDefaultBranch, fetchTree } from './github.js';
7
+ import { print, printStep, printSuccess, printError, BOLD, RESET, DIM, GREEN, } from './output.js';
8
+ const DIRECT_SENTINEL = '';
9
+ /**
10
+ * Validate that pluginPath has the expected path suffix for its type
11
+ * (/SKILL.md for skills, .md for commands/agents).
12
+ */
13
+ function validatePathFormat(componentType, pluginPath) {
14
+ if (componentType === 'skill') {
15
+ if (!pluginPath.endsWith('/SKILL.md')) {
16
+ printError('Invalid skill path: must end with /SKILL.md');
17
+ process.exit(1);
18
+ }
19
+ }
20
+ else {
21
+ if (!pluginPath.endsWith('.md')) {
22
+ printError(`Invalid ${componentType} path: must end with .md`);
23
+ process.exit(1);
24
+ }
25
+ }
26
+ }
27
+ /**
28
+ * Validate that repoPath and pluginPath are aligned.
29
+ * Either they are equal (no subdir) or repoPath ends with '/' + pluginPath (monorepo).
30
+ */
31
+ function validatePathAlignment(repoPath, pluginPath) {
32
+ if (!repoPath || !pluginPath) {
33
+ printError('Both repo path and source path must be non-empty');
34
+ process.exit(1);
35
+ }
36
+ if (repoPath !== pluginPath && !repoPath.endsWith('/' + pluginPath)) {
37
+ printError('Repo path and --source-path do not match: repo path must equal source path or end with /<source-path>');
38
+ process.exit(1);
39
+ }
40
+ }
41
+ /**
42
+ * Derive componentName from pluginPath, matching flattenComponents() semantics in fetch.ts.
43
+ */
44
+ function deriveComponentName(componentType, pluginPath) {
45
+ if (componentType === 'skill') {
46
+ // "skills/review/SKILL.md" → "review"
47
+ // "tools/helpers/my-skill/SKILL.md" → "my-skill"
48
+ const withoutSuffix = pluginPath.replace(/\/SKILL\.md$/, '');
49
+ return withoutSuffix.split('/').pop();
50
+ }
51
+ // Commands/agents: strip type prefix if present, then strip .md
52
+ // "commands/git/commit.md" → "git/commit"
53
+ // "custom/cmds/deploy.md" → "custom/cmds/deploy"
54
+ const typeDir = componentType === 'command' ? 'commands/' : 'agents/';
55
+ const relPath = pluginPath.startsWith(typeDir)
56
+ ? pluginPath.slice(typeDir.length)
57
+ : pluginPath;
58
+ return relPath.replace(/\.md$/, '');
59
+ }
60
+ /**
61
+ * Compute the local destination path for a component.
62
+ */
63
+ function computeDestPath(scope, componentType, pluginPath, componentName) {
64
+ const baseDir = getBaseDir(scope);
65
+ if (componentType === 'skill') {
66
+ return join(baseDir, 'skills', componentName);
67
+ }
68
+ const subdir = componentType === 'command' ? 'commands' : 'agents';
69
+ const prefix = subdir + '/';
70
+ const relPath = pluginPath.startsWith(prefix)
71
+ ? pluginPath.slice(prefix.length)
72
+ : pluginPath;
73
+ return join(baseDir, subdir, relPath);
74
+ }
75
+ export async function runDirectComponentInstall(ownerRepo, componentType, repoPath, pluginPath, yes, scopeOverride) {
76
+ // Step 1: Validate path format and alignment
77
+ validatePathFormat(componentType, pluginPath);
78
+ validatePathAlignment(repoPath, pluginPath);
79
+ // Step 2: Fetch tree, handle truncation
80
+ printStep('Fetching repository tree...');
81
+ const branch = await fetchDefaultBranch(ownerRepo);
82
+ if (!branch) {
83
+ printError(`Could not access repository ${ownerRepo}`);
84
+ process.exit(1);
85
+ }
86
+ const tree = await fetchTree(ownerRepo, branch);
87
+ if (!tree) {
88
+ printError(`Could not fetch file tree for ${ownerRepo}`);
89
+ process.exit(1);
90
+ }
91
+ if (tree.truncated) {
92
+ printError('Repository tree is too large (truncated). Cannot verify component path.');
93
+ printError(`Install the full plugin instead: npx claudepluginhub ${ownerRepo}`);
94
+ process.exit(1);
95
+ }
96
+ // Step 3: Verify exact repoPath in tree
97
+ const exists = tree.tree.some((e) => e.type === 'blob' && e.path === repoPath);
98
+ if (!exists) {
99
+ printError(`File not found: ${repoPath}`);
100
+ printError(`Verify the path exists in ${ownerRepo} on branch ${branch}`);
101
+ process.exit(1);
102
+ }
103
+ // Step 4: Skill — reject nested template SKILL.md
104
+ if (componentType === 'skill') {
105
+ const skillDir = repoPath.replace(/\/SKILL\.md$/, '').toLowerCase();
106
+ const isNested = tree.tree.some((e) => e.type === 'blob' &&
107
+ e.path.toLowerCase().endsWith('/skill.md') &&
108
+ e.path.toLowerCase() !== repoPath.toLowerCase() &&
109
+ skillDir.startsWith(e.path.replace(/\/skill\.md$/i, '').toLowerCase() + '/'));
110
+ if (isNested) {
111
+ printError('Cannot install nested SKILL.md (template or asset file inside another skill directory).');
112
+ process.exit(1);
113
+ }
114
+ }
115
+ // Step 5: Derive component name
116
+ const componentName = deriveComponentName(componentType, pluginPath);
117
+ printSuccess(`${componentName} (${componentType})`);
118
+ // Step 6: Scope picker
119
+ let scope = scopeOverride ?? 'project';
120
+ if (!yes && !scopeOverride) {
121
+ print('');
122
+ const chosenScope = await scopePrompt('project');
123
+ if (chosenScope === null) {
124
+ print('\nCancelled.');
125
+ process.exit(0);
126
+ }
127
+ scope = chosenScope;
128
+ }
129
+ // Step 7: Collision detection (two layers)
130
+ const lock = readLock(scope);
131
+ const existing = lock.collections[ownerRepo];
132
+ const newKey = componentKey(ownerRepo, repoPath);
133
+ // Layer 1: Lock collision — different repoPath, same pluginPath
134
+ if (existing) {
135
+ for (const [key, comp] of Object.entries(existing.components)) {
136
+ if (key !== newKey && comp.sourcePath === pluginPath) {
137
+ printError(`Collision: "${pluginPath}" is already installed from a different path in this repo.`);
138
+ printError(`Existing source: ${key}`);
139
+ printError('Remove the existing install first, or install the full plugin instead.');
140
+ process.exit(1);
141
+ }
142
+ }
143
+ }
144
+ // Layer 2: Filesystem collision — destination already occupied
145
+ const isReinstall = existing?.components[newKey] !== undefined;
146
+ if (!isReinstall) {
147
+ const destPath = computeDestPath(scope, componentType, pluginPath, componentName);
148
+ if (existsSync(destPath)) {
149
+ printError(`Destination already exists: ${destPath}`);
150
+ printError('Another plugin, collection, or manual install may be using this path.');
151
+ printError('Remove the existing file/directory first, or install the full plugin instead.');
152
+ process.exit(1);
153
+ }
154
+ }
155
+ // Step 8: Build FlatComponent and install
156
+ print('');
157
+ printStep(`Installing ${componentName}...`);
158
+ const component = {
159
+ source: ownerRepo,
160
+ type: componentType,
161
+ componentName,
162
+ sourcePath: pluginPath,
163
+ downloadPath: repoPath,
164
+ inlineConfig: null,
165
+ };
166
+ const result = await installComponent(component, scope, false);
167
+ if (result.status !== 'installed') {
168
+ printError(`Failed to install ${componentName}. Check the error messages above for details.`);
169
+ process.exit(1);
170
+ }
171
+ // Step 9: Merge into lock file
172
+ const mergedComponents = {
173
+ ...(existing?.components ?? {}),
174
+ [newKey]: result.lock,
175
+ };
176
+ addCollection(lock, ownerRepo, DIRECT_SENTINEL, scope, mergedComponents);
177
+ writeLock(scope, lock);
178
+ // Step 10: Print result
179
+ printSuccess(componentName);
180
+ print('');
181
+ print(`${GREEN}${BOLD}Done!${RESET} ${componentType} "${componentName}" installed.`);
182
+ print('');
183
+ print(`${DIM}Manage with:${RESET}`);
184
+ print(` ${DIM}npx claudepluginhub remove ${ownerRepo} --scope ${scope}${RESET}`);
185
+ print('');
186
+ }
@@ -5,8 +5,10 @@ export interface FlatComponent {
5
5
  type: 'command' | 'agent' | 'skill' | 'hook' | 'mcp' | 'lsp';
6
6
  componentName: string;
7
7
  sourcePath: string;
8
+ downloadPath?: string;
8
9
  inlineConfig: unknown | null;
9
10
  }
11
+ export declare function getBaseDir(scope: Scope): string;
10
12
  export type InstallResult = {
11
13
  status: 'installed';
12
14
  lock: ComponentLock;
package/dist/download.js CHANGED
@@ -3,7 +3,7 @@ import { dirname, join, resolve, sep } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
4
  import { fetchDefaultBranch, fetchTree, downloadFile, findTreeSha, findBlobSha, listFilesUnder, } from './github.js';
5
5
  import { printError, printProgress } from './output.js';
6
- function getBaseDir(scope) {
6
+ export function getBaseDir(scope) {
7
7
  if (scope === 'user') {
8
8
  return join(homedir(), '.claude');
9
9
  }
@@ -43,8 +43,9 @@ function isSafePath(destPath, baseDir) {
43
43
  }
44
44
  export async function downloadSkill(component, scope) {
45
45
  const { source, sourcePath, componentName } = component;
46
- // Strip /SKILL.md to get directory path
47
- const dirPath = sourcePath.replace(/\/SKILL\.md$/, '');
46
+ const downloadSource = component.downloadPath ?? sourcePath;
47
+ // Strip /SKILL.md to get directory path (repo-root-relative for GitHub API)
48
+ const dirPath = downloadSource.replace(/\/SKILL\.md$/, '');
48
49
  const branch = await fetchDefaultBranch(source);
49
50
  if (!branch)
50
51
  return null;
@@ -99,6 +100,7 @@ export async function downloadSkill(component, scope) {
99
100
  }
100
101
  export async function downloadSingleFile(component, scope) {
101
102
  const { source, type, sourcePath, componentName } = component;
103
+ const effectivePath = component.downloadPath ?? sourcePath;
102
104
  const branch = await fetchDefaultBranch(source);
103
105
  if (!branch)
104
106
  return null;
@@ -115,12 +117,12 @@ export async function downloadSingleFile(component, scope) {
115
117
  printError(`Skipping unsafe path: ${sourcePath}`);
116
118
  return null;
117
119
  }
118
- const content = await downloadFile(source, branch, sourcePath);
120
+ const content = await downloadFile(source, branch, effectivePath);
119
121
  if (content === null)
120
122
  return null;
121
123
  ensureDir(destPath);
122
124
  writeFileSync(destPath, content);
123
- const hash = findBlobSha(tree, sourcePath) ?? 'unknown';
125
+ const hash = findBlobSha(tree, effectivePath) ?? 'unknown';
124
126
  return {
125
127
  source,
126
128
  type,
package/dist/index.js CHANGED
@@ -3,11 +3,13 @@ import { resolveMarketplaceUrl, fetchMarketplace, flattenComponents } from './fe
3
3
  import { checkboxPrompt, scopePrompt, confirmPrompt } from './prompts.js';
4
4
  import { runInstall } from './install.js';
5
5
  import { runUpdate } from './update.js';
6
- import { runRemove } from './remove.js';
6
+ import { runRemove, runDirectRemove } 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
+ import { findWrapperScopes } from './wrapper.js';
11
+ import { findDirectInstallScopes } from './scope-detect.js';
12
+ import { parseArgs, validatePlugin, validateComponentFlag } from './args.js';
11
13
  import { print, printBanner, printStep, printSuccess, printError, DIM, RESET, GREEN, CYAN, YELLOW, } from './output.js';
12
14
  const TYPE_BADGES = {
13
15
  skill: `${GREEN}[skill]${RESET}`,
@@ -23,8 +25,8 @@ function printUsage() {
23
25
  Commands:
24
26
  <identifier> Install from a source
25
27
  add <identifier> Same as above
26
- update [identifier] Update (u/ collections only)
27
- list List (u/ collections only)
28
+ update [identifier] Update installed collections
29
+ list List all installed collections
28
30
  remove <identifier> Remove
29
31
 
30
32
  Identifiers:
@@ -32,16 +34,26 @@ Identifiers:
32
34
  u/<userId>/<slug> ClaudePluginHub collection
33
35
  https://claudepluginhub.com/api/user-plugins/.../marketplace.json
34
36
 
35
- Note: owner/repo installs use Claude Code's plugin system.
36
- - list/update only show u/ installs
37
+ Install modes:
38
+ owner/repo Full plugin install (via Claude Code plugin system)
39
+ owner/repo --skill <path> Direct single-component install (skill, command, or agent)
40
+ u/<userId>/<slug> Collection install
41
+
42
+ Notes:
43
+ - list shows all lock-backed installs (collections + direct components)
44
+ - update applies to collections only; direct installs are skipped
37
45
  - Use \`claude plugin list\` for Claude Code plugins
38
46
  - Standalone: \`npx claudepluginhub remove owner/repo\` for cleanup
39
47
 
40
48
  Options:
41
- --plugin <name> Install a specific plugin from a marketplace repo
42
- --yes, -y Skip prompts (install all, project scope)
43
- --scope <scope> Set scope: user, project (default), local
44
- -h, --help Show this help
49
+ --plugin <name> Install a specific plugin from a marketplace repo
50
+ --skill <path> Install a single skill directly from a repo
51
+ --command <path> Install a single command directly from a repo
52
+ --agent <path> Install a single agent directly from a repo
53
+ --source-path <path> Plugin-relative path (for monorepo installs)
54
+ --yes, -y Skip prompts (install all, project scope)
55
+ --scope <scope> Set scope: user, project (default), local
56
+ -h, --help Show this help
45
57
  `);
46
58
  }
47
59
  async function main() {
@@ -53,7 +65,8 @@ async function main() {
53
65
  }
54
66
  const parsed = parseArgs(args);
55
67
  validatePlugin(parsed);
56
- const { command, identifier, plugin, yes, scope: scopeOverride } = parsed;
68
+ validateComponentFlag(parsed);
69
+ const { command, identifier, plugin, yes, scope: scopeOverride, componentFlag } = parsed;
57
70
  printBanner();
58
71
  // Handle non-install commands
59
72
  if (command === 'list') {
@@ -69,9 +82,42 @@ async function main() {
69
82
  printError('Missing identifier. Usage: npx claudepluginhub remove <identifier>');
70
83
  process.exit(1);
71
84
  }
72
- // owner/repo remove → github wrapper cleanup
73
85
  if (isOwnerRepo(identifier)) {
74
- await runGithubRemove(identifier, scopeOverride);
86
+ // Scope-aware remove: check both direct installs and wrapper installs
87
+ const directScopes = findDirectInstallScopes(identifier);
88
+ const wrapperScopes = findWrapperScopes(identifier);
89
+ const allMatches = [
90
+ ...directScopes.map((s) => ({ scope: s, kind: 'direct' })),
91
+ ...wrapperScopes.map((s) => ({ scope: s, kind: 'wrapper' })),
92
+ ];
93
+ const filtered = scopeOverride
94
+ ? allMatches.filter((m) => m.scope === scopeOverride)
95
+ : allMatches;
96
+ // Case 1: Nothing found anywhere → fall back for marketplace guidance
97
+ if (allMatches.length === 0) {
98
+ await runGithubRemove(identifier, scopeOverride);
99
+ process.exit(0);
100
+ }
101
+ // Case 2: Found in other scopes but not the requested one
102
+ if (filtered.length === 0) {
103
+ const scopeList = [...new Set(allMatches.map((m) => m.scope))].join(', ');
104
+ printError(`"${identifier}" not found in ${scopeOverride} scope (found in: ${scopeList})`);
105
+ process.exit(1);
106
+ }
107
+ // Case 3: Multiple scopes without --scope
108
+ const uniqueScopes = [...new Set(filtered.map((m) => m.scope))];
109
+ if (uniqueScopes.length > 1) {
110
+ printError(`"${identifier}" found in multiple scopes: ${uniqueScopes.join(', ')}. Use --scope to specify.`);
111
+ process.exit(1);
112
+ }
113
+ // Case 4: Single scope — remove all matching kinds
114
+ const targetScope = uniqueScopes[0];
115
+ if (filtered.some((m) => m.kind === 'direct')) {
116
+ await runDirectRemove(identifier, targetScope);
117
+ }
118
+ if (filtered.some((m) => m.kind === 'wrapper')) {
119
+ await runGithubRemove(identifier, targetScope);
120
+ }
75
121
  process.exit(0);
76
122
  }
77
123
  await runRemove(identifier);
@@ -82,6 +128,12 @@ async function main() {
82
128
  printUsage();
83
129
  process.exit(1);
84
130
  }
131
+ // Direct component install: owner/repo + --skill/--command/--agent
132
+ if (componentFlag && isOwnerRepo(identifier)) {
133
+ const { runDirectComponentInstall } = await import('./component-install.js');
134
+ await runDirectComponentInstall(identifier, componentFlag.type, componentFlag.repoPath, componentFlag.pluginPath, yes, scopeOverride);
135
+ process.exit(0);
136
+ }
85
137
  // owner/repo → native Claude Code install
86
138
  if (isOwnerRepo(identifier)) {
87
139
  await runGithubInstall(identifier, yes, scopeOverride, plugin);
package/dist/remove.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { Scope } from './prompts.js';
1
2
  import type { ComponentLock } from './lock.js';
2
3
  export declare function removeComponentByLock(comp: ComponentLock): void;
4
+ export declare function runDirectRemove(identifier: string, scope: Scope): Promise<void>;
3
5
  export declare function runRemove(identifier: string): Promise<void>;
package/dist/remove.js CHANGED
@@ -85,6 +85,24 @@ function removeFiles(lock) {
85
85
  export function removeComponentByLock(comp) {
86
86
  removeFiles(comp);
87
87
  }
88
+ export async function runDirectRemove(identifier, scope) {
89
+ const lock = readLock(scope);
90
+ const collection = lock.collections[identifier];
91
+ if (!collection) {
92
+ printError(`"${identifier}" not found in ${scope} scope`);
93
+ process.exit(1);
94
+ }
95
+ printStep(`Removing ${Object.keys(collection.components).length} component(s) from ${scope} scope...`);
96
+ for (const [, comp] of Object.entries(collection.components)) {
97
+ removeFiles(comp);
98
+ printSuccess(`${comp.componentName} (${comp.type})`);
99
+ }
100
+ removeCollection(lock, identifier);
101
+ writeLock(scope, lock);
102
+ print('');
103
+ print(`${BOLD}Removed${RESET} ${identifier}`);
104
+ print('');
105
+ }
88
106
  export async function runRemove(identifier) {
89
107
  // Try all scopes to find the collection
90
108
  const scopes = ['project', 'local', 'user'];
@@ -0,0 +1,2 @@
1
+ import type { Scope } from './prompts.js';
2
+ export declare function findDirectInstallScopes(identifier: string): Scope[];
@@ -0,0 +1,12 @@
1
+ import { readLock } from './lock.js';
2
+ export function findDirectInstallScopes(identifier) {
3
+ const found = [];
4
+ for (const scope of ['project', 'local', 'user']) {
5
+ const lock = readLock(scope);
6
+ const collection = lock.collections[identifier];
7
+ if (collection && collection.apiUrl === '') {
8
+ found.push(scope);
9
+ }
10
+ }
11
+ return found;
12
+ }
package/dist/update.js CHANGED
@@ -29,6 +29,14 @@ export async function runUpdate(identifier) {
29
29
  if (identifier && collection.identifier !== identifier)
30
30
  continue;
31
31
  found = true;
32
+ // Skip direct installs (apiUrl === '' sentinel)
33
+ if (collection.apiUrl === '') {
34
+ print(`${BOLD}${collection.identifier}${RESET} — direct install, skipped`);
35
+ for (const comp of Object.values(collection.components)) {
36
+ print(` ${DIM}Re-install: npx claudepluginhub ${collection.identifier} --${comp.type} ${comp.sourcePath}${RESET}`);
37
+ }
38
+ continue;
39
+ }
32
40
  print(`${BOLD}Updating${RESET} ${collection.identifier}...`);
33
41
  const manifest = await fetchMarketplace(collection.apiUrl);
34
42
  if (!manifest) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudepluginhub",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "Install Claude Code components from ClaudePluginHub",
5
5
  "bin": {
6
6
  "claudepluginhub": "dist/index.js"