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