@plosson/agentio 0.1.23 → 0.1.24
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/package.json
CHANGED
package/src/commands/claude.ts
CHANGED
|
@@ -1,183 +1,267 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
-
import
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
4
|
import { CliError, handleError } from '../utils/errors';
|
|
5
5
|
import {
|
|
6
6
|
loadAgentioJson,
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
addMarketplace,
|
|
8
|
+
addPlugin,
|
|
9
|
+
removeMarketplace,
|
|
9
10
|
removePlugin,
|
|
10
|
-
getPlugin,
|
|
11
11
|
} from '../services/claude-plugin/agentio-json';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Execute a claude CLI command and return the result.
|
|
15
|
+
*/
|
|
16
|
+
async function execClaude(
|
|
17
|
+
args: string[]
|
|
18
|
+
): Promise<{ success: boolean; stdout: string; stderr: string }> {
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
const proc = spawn('claude', args, {
|
|
21
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
let stdout = '';
|
|
25
|
+
let stderr = '';
|
|
26
|
+
|
|
27
|
+
proc.stdout?.on('data', (data) => {
|
|
28
|
+
stdout += data.toString();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
proc.stderr?.on('data', (data) => {
|
|
32
|
+
stderr += data.toString();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
proc.on('close', (code) => {
|
|
36
|
+
resolve({
|
|
37
|
+
success: code === 0,
|
|
38
|
+
stdout,
|
|
39
|
+
stderr,
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
proc.on('error', (err) => {
|
|
44
|
+
resolve({
|
|
45
|
+
success: false,
|
|
46
|
+
stdout: '',
|
|
47
|
+
stderr: err.message,
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Install a marketplace by calling claude plugin marketplace add.
|
|
55
|
+
* Silently skips if already installed.
|
|
56
|
+
*/
|
|
57
|
+
async function installMarketplace(url: string): Promise<boolean> {
|
|
58
|
+
console.error(`Adding marketplace: ${url}`);
|
|
59
|
+
const result = await execClaude(['plugin', 'marketplace', 'add', url]);
|
|
60
|
+
|
|
61
|
+
if (result.success) {
|
|
62
|
+
console.log(` Added: ${url}`);
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check if already installed (skip silently)
|
|
67
|
+
const errLower = result.stderr.toLowerCase();
|
|
68
|
+
if (errLower.includes('already') || errLower.includes('exists')) {
|
|
69
|
+
console.log(` Skipped (already added): ${url}`);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.error(` Failed: ${result.stderr.trim()}`);
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Install a plugin by calling claude plugin install --scope project.
|
|
79
|
+
*/
|
|
80
|
+
async function installPluginCmd(name: string): Promise<boolean> {
|
|
81
|
+
console.error(`Installing plugin: ${name}`);
|
|
82
|
+
const result = await execClaude(['plugin', 'install', name, '--scope', 'project']);
|
|
83
|
+
|
|
84
|
+
if (result.success) {
|
|
85
|
+
console.log(` Installed: ${name}`);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.error(` Failed: ${result.stderr.trim()}`);
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Uninstall a plugin by calling claude plugin uninstall --scope project.
|
|
95
|
+
*/
|
|
96
|
+
async function uninstallPluginCmd(name: string): Promise<boolean> {
|
|
97
|
+
console.error(`Uninstalling plugin: ${name}`);
|
|
98
|
+
const result = await execClaude(['plugin', 'uninstall', name, '--scope', 'project']);
|
|
99
|
+
|
|
100
|
+
if (result.success) {
|
|
101
|
+
console.log(` Uninstalled: ${name}`);
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Check if not installed (skip silently)
|
|
106
|
+
const errLower = result.stderr.toLowerCase();
|
|
107
|
+
if (errLower.includes('not installed') || errLower.includes('not found')) {
|
|
108
|
+
console.log(` Skipped (not installed): ${name}`);
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
console.error(` Failed: ${result.stderr.trim()}`);
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
17
115
|
|
|
18
116
|
export function registerClaudeCommands(program: Command): void {
|
|
19
117
|
const claude = program
|
|
20
118
|
.command('claude')
|
|
21
119
|
.description('Claude Code plugin operations');
|
|
22
120
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
.
|
|
28
|
-
.
|
|
29
|
-
.
|
|
30
|
-
.option('--
|
|
31
|
-
.
|
|
32
|
-
.option('--agents', 'Install only agents')
|
|
33
|
-
.option('-f, --force', 'Force reinstall if already exists')
|
|
34
|
-
.option('-d, --dir <path>', 'Target directory (default: current directory)')
|
|
35
|
-
.option('-v, --verbose', 'Show detailed installation logs')
|
|
36
|
-
.action(async (source, options) => {
|
|
121
|
+
// install command group
|
|
122
|
+
const install = claude.command('install').description('Install marketplaces and plugins');
|
|
123
|
+
|
|
124
|
+
install
|
|
125
|
+
.command('marketplace')
|
|
126
|
+
.description('Add a plugin marketplace')
|
|
127
|
+
.argument('<url>', 'Marketplace GitHub URL')
|
|
128
|
+
.option('-d, --dir <path>', 'Directory with agentio.json (default: current directory)')
|
|
129
|
+
.action(async (url, options) => {
|
|
37
130
|
try {
|
|
38
131
|
const targetDir = options.dir ? path.resolve(options.dir) : process.cwd();
|
|
39
132
|
|
|
40
|
-
|
|
41
|
-
|
|
133
|
+
const success = await installMarketplace(url);
|
|
134
|
+
if (success) {
|
|
135
|
+
addMarketplace(targetDir, url);
|
|
42
136
|
}
|
|
137
|
+
} catch (error) {
|
|
138
|
+
handleError(error);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
43
141
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
hooks: options.hooks,
|
|
53
|
-
agents: options.agents,
|
|
54
|
-
force: options.force,
|
|
55
|
-
targetDir,
|
|
56
|
-
verbose: options.verbose,
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
console.log(`\nInstalled: ${result.manifest.name} v${result.manifest.version}`);
|
|
60
|
-
if (result.installed.length > 0) {
|
|
61
|
-
console.log(`Components: ${result.installed.length}`);
|
|
62
|
-
for (const comp of result.installed) {
|
|
63
|
-
console.log(` ${comp.type}/${comp.name}`);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
} else {
|
|
67
|
-
// Install all plugins from agentio.json
|
|
68
|
-
if (!agentioJsonExists(targetDir)) {
|
|
69
|
-
throw new CliError(
|
|
70
|
-
'NOT_FOUND',
|
|
71
|
-
'No agentio.json found',
|
|
72
|
-
'Run: agentio claude plugin install <source> to install a plugin'
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const agentioJson = loadAgentioJson(targetDir);
|
|
77
|
-
const plugins = Object.entries(agentioJson.plugins);
|
|
78
|
-
|
|
79
|
-
if (plugins.length === 0) {
|
|
80
|
-
console.log('No plugins defined in agentio.json');
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
console.error(`Installing ${plugins.length} plugin(s) from agentio.json...`);
|
|
85
|
-
console.error(`Target: ${path.join(targetDir, '.claude')}`);
|
|
86
|
-
|
|
87
|
-
let installed = 0;
|
|
88
|
-
for (const [name, entry] of plugins) {
|
|
89
|
-
console.error(`\nInstalling ${name}...`);
|
|
90
|
-
|
|
91
|
-
// Determine component flags based on entry.components
|
|
92
|
-
const installOptions = {
|
|
93
|
-
skills:
|
|
94
|
-
!entry.components || entry.components.includes('skills'),
|
|
95
|
-
commands:
|
|
96
|
-
!entry.components || entry.components.includes('commands'),
|
|
97
|
-
hooks: !entry.components || entry.components.includes('hooks'),
|
|
98
|
-
agents: !entry.components || entry.components.includes('agents'),
|
|
99
|
-
force: options.force,
|
|
100
|
-
targetDir,
|
|
101
|
-
verbose: options.verbose,
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
try {
|
|
105
|
-
const result = await installPlugin(entry.source, installOptions);
|
|
106
|
-
console.log(` Installed: ${result.manifest.name} v${result.manifest.version}`);
|
|
107
|
-
installed++;
|
|
108
|
-
} catch (error) {
|
|
109
|
-
console.error(` Failed to install ${name}: ${error}`);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
142
|
+
install
|
|
143
|
+
.command('plugin')
|
|
144
|
+
.description('Install a plugin')
|
|
145
|
+
.argument('<name>', 'Plugin name (e.g., plugin-name@marketplace)')
|
|
146
|
+
.option('-d, --dir <path>', 'Directory with agentio.json (default: current directory)')
|
|
147
|
+
.action(async (name, options) => {
|
|
148
|
+
try {
|
|
149
|
+
const targetDir = options.dir ? path.resolve(options.dir) : process.cwd();
|
|
112
150
|
|
|
113
|
-
|
|
151
|
+
const success = await installPluginCmd(name);
|
|
152
|
+
if (success) {
|
|
153
|
+
addPlugin(targetDir, name);
|
|
114
154
|
}
|
|
115
155
|
} catch (error) {
|
|
116
156
|
handleError(error);
|
|
117
157
|
}
|
|
118
158
|
});
|
|
119
159
|
|
|
120
|
-
|
|
160
|
+
// install with no subcommand - install all from agentio.json
|
|
161
|
+
install.action(async (options) => {
|
|
162
|
+
try {
|
|
163
|
+
const targetDir = options.dir ? path.resolve(options.dir) : process.cwd();
|
|
164
|
+
const config = loadAgentioJson(targetDir);
|
|
165
|
+
|
|
166
|
+
if (config.marketplaces.length === 0 && config.plugins.length === 0) {
|
|
167
|
+
console.log('No marketplaces or plugins defined in agentio.json');
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
console.error(`Installing from agentio.json...`);
|
|
172
|
+
|
|
173
|
+
// Install marketplaces first
|
|
174
|
+
if (config.marketplaces.length > 0) {
|
|
175
|
+
console.error(`\nMarketplaces (${config.marketplaces.length}):`);
|
|
176
|
+
for (const url of config.marketplaces) {
|
|
177
|
+
await installMarketplace(url);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Then install plugins
|
|
182
|
+
if (config.plugins.length > 0) {
|
|
183
|
+
console.error(`\nPlugins (${config.plugins.length}):`);
|
|
184
|
+
for (const name of config.plugins) {
|
|
185
|
+
await installPluginCmd(name);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
console.log('\nDone.');
|
|
190
|
+
} catch (error) {
|
|
191
|
+
handleError(error);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// list command
|
|
196
|
+
claude
|
|
121
197
|
.command('list')
|
|
122
|
-
.description('List plugins from agentio.json')
|
|
198
|
+
.description('List marketplaces and plugins from agentio.json')
|
|
123
199
|
.option('-d, --dir <path>', 'Directory with agentio.json (default: current directory)')
|
|
124
200
|
.action(async (options) => {
|
|
125
201
|
try {
|
|
126
202
|
const targetDir = options.dir ? path.resolve(options.dir) : process.cwd();
|
|
203
|
+
const config = loadAgentioJson(targetDir);
|
|
127
204
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (plugins.length === 0) {
|
|
131
|
-
console.log('No plugins in agentio.json');
|
|
205
|
+
if (config.marketplaces.length === 0 && config.plugins.length === 0) {
|
|
206
|
+
console.log('No marketplaces or plugins defined in agentio.json');
|
|
132
207
|
return;
|
|
133
208
|
}
|
|
134
209
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
210
|
+
if (config.marketplaces.length > 0) {
|
|
211
|
+
console.log(`Marketplaces (${config.marketplaces.length}):`);
|
|
212
|
+
for (const url of config.marketplaces) {
|
|
213
|
+
console.log(` ${url}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (config.plugins.length > 0) {
|
|
218
|
+
if (config.marketplaces.length > 0) {
|
|
219
|
+
console.log('');
|
|
220
|
+
}
|
|
221
|
+
console.log(`Plugins (${config.plugins.length}):`);
|
|
222
|
+
for (const name of config.plugins) {
|
|
223
|
+
console.log(` ${name}`);
|
|
141
224
|
}
|
|
142
|
-
console.log('');
|
|
143
225
|
}
|
|
144
226
|
} catch (error) {
|
|
145
227
|
handleError(error);
|
|
146
228
|
}
|
|
147
229
|
});
|
|
148
230
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
231
|
+
// remove command group
|
|
232
|
+
const remove = claude.command('remove').description('Remove marketplaces and plugins');
|
|
233
|
+
|
|
234
|
+
remove
|
|
235
|
+
.command('marketplace')
|
|
236
|
+
.description('Remove a marketplace from agentio.json')
|
|
237
|
+
.argument('<url>', 'Marketplace URL to remove')
|
|
153
238
|
.option('-d, --dir <path>', 'Directory with agentio.json (default: current directory)')
|
|
154
|
-
.action(async (
|
|
239
|
+
.action(async (url, options) => {
|
|
155
240
|
try {
|
|
156
241
|
const targetDir = options.dir ? path.resolve(options.dir) : process.cwd();
|
|
157
242
|
|
|
158
|
-
const
|
|
159
|
-
if (
|
|
160
|
-
|
|
243
|
+
const removed = removeMarketplace(targetDir, url);
|
|
244
|
+
if (removed) {
|
|
245
|
+
console.log(`Removed marketplace: ${url}`);
|
|
246
|
+
} else {
|
|
247
|
+
throw new CliError('NOT_FOUND', `Marketplace not found: ${url}`);
|
|
161
248
|
}
|
|
249
|
+
} catch (error) {
|
|
250
|
+
handleError(error);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
162
253
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
254
|
+
remove
|
|
255
|
+
.command('plugin')
|
|
256
|
+
.description('Uninstall a plugin and remove from agentio.json')
|
|
257
|
+
.argument('<name>', 'Plugin name to remove')
|
|
258
|
+
.option('-d, --dir <path>', 'Directory with agentio.json (default: current directory)')
|
|
259
|
+
.action(async (name, options) => {
|
|
260
|
+
try {
|
|
261
|
+
const targetDir = options.dir ? path.resolve(options.dir) : process.cwd();
|
|
170
262
|
|
|
171
|
-
|
|
263
|
+
await uninstallPluginCmd(name);
|
|
172
264
|
removePlugin(targetDir, name);
|
|
173
|
-
|
|
174
|
-
console.log(`Removed: ${name}`);
|
|
175
|
-
if (components.length > 0) {
|
|
176
|
-
console.log(`Removed components: ${components.length}`);
|
|
177
|
-
for (const comp of components) {
|
|
178
|
-
console.log(` ${comp.type}/${comp.name}`);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
265
|
} catch (error) {
|
|
182
266
|
handleError(error);
|
|
183
267
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
-
import type { AgentioJson
|
|
3
|
+
import type { AgentioJson } from '../../types/claude-plugin';
|
|
4
4
|
|
|
5
5
|
const AGENTIO_JSON_FILE = 'agentio.json';
|
|
6
6
|
|
|
@@ -19,15 +19,18 @@ export function loadAgentioJson(dir: string): AgentioJson {
|
|
|
19
19
|
const filePath = getAgentioJsonPath(dir);
|
|
20
20
|
|
|
21
21
|
if (!fs.existsSync(filePath)) {
|
|
22
|
-
return { plugins:
|
|
22
|
+
return { marketplaces: [], plugins: [] };
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
26
26
|
const data = JSON.parse(content) as AgentioJson;
|
|
27
27
|
|
|
28
|
-
// Ensure
|
|
28
|
+
// Ensure arrays exist
|
|
29
|
+
if (!data.marketplaces) {
|
|
30
|
+
data.marketplaces = [];
|
|
31
|
+
}
|
|
29
32
|
if (!data.plugins) {
|
|
30
|
-
data.plugins =
|
|
33
|
+
data.plugins = [];
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
return data;
|
|
@@ -43,61 +46,53 @@ export function saveAgentioJson(dir: string, data: AgentioJson): void {
|
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
/**
|
|
46
|
-
*
|
|
49
|
+
* Add a marketplace URL if not already present.
|
|
47
50
|
*/
|
|
48
|
-
export function
|
|
49
|
-
|
|
51
|
+
export function addMarketplace(dir: string, url: string): void {
|
|
52
|
+
const data = loadAgentioJson(dir);
|
|
53
|
+
if (!data.marketplaces.includes(url)) {
|
|
54
|
+
data.marketplaces.push(url);
|
|
55
|
+
saveAgentioJson(dir, data);
|
|
56
|
+
}
|
|
50
57
|
}
|
|
51
58
|
|
|
52
59
|
/**
|
|
53
|
-
* Add
|
|
60
|
+
* Add a plugin name if not already present.
|
|
54
61
|
*/
|
|
55
|
-
export function addPlugin(
|
|
56
|
-
dir: string,
|
|
57
|
-
name: string,
|
|
58
|
-
entry: AgentioPluginEntry
|
|
59
|
-
): void {
|
|
62
|
+
export function addPlugin(dir: string, name: string): void {
|
|
60
63
|
const data = loadAgentioJson(dir);
|
|
61
|
-
data.plugins
|
|
62
|
-
|
|
64
|
+
if (!data.plugins.includes(name)) {
|
|
65
|
+
data.plugins.push(name);
|
|
66
|
+
saveAgentioJson(dir, data);
|
|
67
|
+
}
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
/**
|
|
66
|
-
* Remove a
|
|
67
|
-
* Returns true if
|
|
71
|
+
* Remove a marketplace URL.
|
|
72
|
+
* Returns true if found and removed.
|
|
68
73
|
*/
|
|
69
|
-
export function
|
|
74
|
+
export function removeMarketplace(dir: string, url: string): boolean {
|
|
70
75
|
const data = loadAgentioJson(dir);
|
|
71
|
-
|
|
72
|
-
if (
|
|
76
|
+
const index = data.marketplaces.indexOf(url);
|
|
77
|
+
if (index === -1) {
|
|
73
78
|
return false;
|
|
74
79
|
}
|
|
75
|
-
|
|
76
|
-
delete data.plugins[name];
|
|
80
|
+
data.marketplaces.splice(index, 1);
|
|
77
81
|
saveAgentioJson(dir, data);
|
|
78
82
|
return true;
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
/**
|
|
82
|
-
*
|
|
83
|
-
|
|
84
|
-
export function getPlugin(
|
|
85
|
-
dir: string,
|
|
86
|
-
name: string
|
|
87
|
-
): AgentioPluginEntry | undefined {
|
|
88
|
-
const data = loadAgentioJson(dir);
|
|
89
|
-
return data.plugins[name];
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* List all plugins in agentio.json.
|
|
86
|
+
* Remove a plugin name.
|
|
87
|
+
* Returns true if found and removed.
|
|
94
88
|
*/
|
|
95
|
-
export function
|
|
96
|
-
dir: string
|
|
97
|
-
): Array<{ name: string; entry: AgentioPluginEntry }> {
|
|
89
|
+
export function removePlugin(dir: string, name: string): boolean {
|
|
98
90
|
const data = loadAgentioJson(dir);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
91
|
+
const index = data.plugins.indexOf(name);
|
|
92
|
+
if (index === -1) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
data.plugins.splice(index, 1);
|
|
96
|
+
saveAgentioJson(dir, data);
|
|
97
|
+
return true;
|
|
103
98
|
}
|
|
@@ -1,65 +1,5 @@
|
|
|
1
|
-
//
|
|
2
|
-
export interface ParsedSource {
|
|
3
|
-
owner: string;
|
|
4
|
-
repo: string;
|
|
5
|
-
branch?: string; // undefined = default branch
|
|
6
|
-
path?: string; // path within repo to plugin root
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
// Plugin manifest (.claude-plugin/plugin.json)
|
|
10
|
-
export interface PluginManifest {
|
|
11
|
-
name: string;
|
|
12
|
-
version: string;
|
|
13
|
-
description?: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// Component types that can be installed
|
|
17
|
-
export type ComponentType = 'skills' | 'commands' | 'hooks' | 'agents';
|
|
18
|
-
|
|
19
|
-
// Discovered components from filesystem
|
|
20
|
-
export interface DiscoveredComponents {
|
|
21
|
-
skills: string[];
|
|
22
|
-
commands: string[];
|
|
23
|
-
hooks: string[];
|
|
24
|
-
agents: string[];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Installation options
|
|
28
|
-
export interface PluginInstallOptions {
|
|
29
|
-
skills?: boolean;
|
|
30
|
-
commands?: boolean;
|
|
31
|
-
hooks?: boolean;
|
|
32
|
-
agents?: boolean;
|
|
33
|
-
force?: boolean;
|
|
34
|
-
targetDir?: string;
|
|
35
|
-
verbose?: boolean;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Single installed component record
|
|
39
|
-
export interface InstalledComponent {
|
|
40
|
-
name: string;
|
|
41
|
-
type: ComponentType;
|
|
42
|
-
path: string; // Relative to agentio.json
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Plugin entry in agentio.json
|
|
46
|
-
export interface AgentioPluginEntry {
|
|
47
|
-
source: string;
|
|
48
|
-
version: string;
|
|
49
|
-
components?: ComponentType[];
|
|
50
|
-
installedComponents: InstalledComponent[];
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// agentio.json structure
|
|
1
|
+
// agentio.json structure for Claude plugin installation
|
|
54
2
|
export interface AgentioJson {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Installation result
|
|
61
|
-
export interface InstallResult {
|
|
62
|
-
success: boolean;
|
|
63
|
-
manifest: PluginManifest;
|
|
64
|
-
installed: InstalledComponent[];
|
|
3
|
+
marketplaces: string[];
|
|
4
|
+
plugins: string[];
|
|
65
5
|
}
|
|
@@ -1,405 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import { execSync } from 'child_process';
|
|
4
|
-
import { tmpdir } from 'os';
|
|
5
|
-
import { CliError } from '../../utils/errors';
|
|
6
|
-
import type {
|
|
7
|
-
ParsedSource,
|
|
8
|
-
PluginManifest,
|
|
9
|
-
DiscoveredComponents,
|
|
10
|
-
PluginInstallOptions,
|
|
11
|
-
InstalledComponent,
|
|
12
|
-
InstallResult,
|
|
13
|
-
ComponentType,
|
|
14
|
-
} from '../../types/claude-plugin';
|
|
15
|
-
import { parseSource, buildGitCloneUrl } from './source-parser';
|
|
16
|
-
import { addPlugin } from './agentio-json';
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Clone a repository to a temporary directory.
|
|
20
|
-
*/
|
|
21
|
-
function cloneRepo(parsed: ParsedSource): string {
|
|
22
|
-
const tempDir = fs.mkdtempSync(path.join(tmpdir(), 'agentio-plugin-'));
|
|
23
|
-
const cloneUrl = buildGitCloneUrl(parsed);
|
|
24
|
-
|
|
25
|
-
let cmd = `git clone --depth 1`;
|
|
26
|
-
if (parsed.branch) {
|
|
27
|
-
cmd += ` -b ${parsed.branch}`;
|
|
28
|
-
}
|
|
29
|
-
cmd += ` "${cloneUrl}" "${tempDir}"`;
|
|
30
|
-
|
|
31
|
-
try {
|
|
32
|
-
execSync(cmd, { stdio: 'pipe' });
|
|
33
|
-
} catch {
|
|
34
|
-
// Clean up temp dir on clone failure
|
|
35
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
36
|
-
throw new CliError(
|
|
37
|
-
'API_ERROR',
|
|
38
|
-
`Failed to clone repository: ${parsed.owner}/${parsed.repo}`,
|
|
39
|
-
'Check the repository URL and your network connection'
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return tempDir;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Clean up a temporary directory.
|
|
48
|
-
*/
|
|
49
|
-
function cleanupTempDir(tempDir: string): void {
|
|
50
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Read the plugin manifest from a cloned repository.
|
|
55
|
-
*/
|
|
56
|
-
function readPluginManifest(
|
|
57
|
-
repoDir: string,
|
|
58
|
-
parsed: ParsedSource
|
|
59
|
-
): PluginManifest {
|
|
60
|
-
const basePath = parsed.path ? path.join(repoDir, parsed.path) : repoDir;
|
|
61
|
-
const manifestPath = path.join(basePath, '.claude-plugin', 'plugin.json');
|
|
62
|
-
|
|
63
|
-
if (!fs.existsSync(manifestPath)) {
|
|
64
|
-
throw new CliError(
|
|
65
|
-
'NOT_FOUND',
|
|
66
|
-
'Plugin manifest not found',
|
|
67
|
-
'Ensure .claude-plugin/plugin.json exists in the plugin root'
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
try {
|
|
72
|
-
const content = fs.readFileSync(manifestPath, 'utf-8');
|
|
73
|
-
const manifest = JSON.parse(content) as PluginManifest;
|
|
74
|
-
|
|
75
|
-
if (!manifest.name || !manifest.version) {
|
|
76
|
-
throw new CliError(
|
|
77
|
-
'INVALID_PARAMS',
|
|
78
|
-
'Invalid plugin manifest: missing name or version'
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return manifest;
|
|
83
|
-
} catch (error) {
|
|
84
|
-
if (error instanceof CliError) throw error;
|
|
85
|
-
if (error instanceof SyntaxError) {
|
|
86
|
-
throw new CliError('INVALID_PARAMS', 'Invalid plugin manifest JSON');
|
|
87
|
-
}
|
|
88
|
-
throw error;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Discover available components in a cloned repository.
|
|
94
|
-
*/
|
|
95
|
-
function discoverComponents(
|
|
96
|
-
repoDir: string,
|
|
97
|
-
parsed: ParsedSource
|
|
98
|
-
): DiscoveredComponents {
|
|
99
|
-
const basePath = parsed.path ? path.join(repoDir, parsed.path) : repoDir;
|
|
100
|
-
const result: DiscoveredComponents = {
|
|
101
|
-
skills: [],
|
|
102
|
-
commands: [],
|
|
103
|
-
hooks: [],
|
|
104
|
-
agents: [],
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
const componentTypes: ComponentType[] = ['skills', 'commands', 'hooks', 'agents'];
|
|
108
|
-
|
|
109
|
-
for (const type of componentTypes) {
|
|
110
|
-
const typePath = path.join(basePath, type);
|
|
111
|
-
if (fs.existsSync(typePath)) {
|
|
112
|
-
const entries = fs.readdirSync(typePath, { withFileTypes: true });
|
|
113
|
-
result[type] = entries
|
|
114
|
-
.filter((e) => e.isDirectory())
|
|
115
|
-
.map((e) => e.name);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return result;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Copy a component from the cloned repository to the target directory.
|
|
124
|
-
*/
|
|
125
|
-
function copyComponent(
|
|
126
|
-
repoDir: string,
|
|
127
|
-
parsed: ParsedSource,
|
|
128
|
-
componentType: ComponentType,
|
|
129
|
-
componentName: string,
|
|
130
|
-
targetDir: string
|
|
131
|
-
): void {
|
|
132
|
-
const basePath = parsed.path ? path.join(repoDir, parsed.path) : repoDir;
|
|
133
|
-
const srcPath = path.join(basePath, componentType, componentName);
|
|
134
|
-
const destPath = path.join(targetDir, '.claude', componentType, componentName);
|
|
135
|
-
|
|
136
|
-
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
137
|
-
fs.cpSync(srcPath, destPath, { recursive: true });
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Determine which components to install based on options.
|
|
142
|
-
*/
|
|
143
|
-
function determineComponentsToInstall(
|
|
144
|
-
options: PluginInstallOptions,
|
|
145
|
-
discovered: DiscoveredComponents
|
|
146
|
-
): DiscoveredComponents {
|
|
147
|
-
// If no specific flags, install all
|
|
148
|
-
const installAll = !options.skills && !options.commands && !options.hooks && !options.agents;
|
|
149
|
-
|
|
150
|
-
return {
|
|
151
|
-
skills: installAll || options.skills ? discovered.skills : [],
|
|
152
|
-
commands: installAll || options.commands ? discovered.commands : [],
|
|
153
|
-
hooks: installAll || options.hooks ? discovered.hooks : [],
|
|
154
|
-
agents: installAll || options.agents ? discovered.agents : [],
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Get the component types array for agentio.json based on what was installed.
|
|
160
|
-
*/
|
|
161
|
-
function getInstalledComponentTypes(
|
|
162
|
-
options: PluginInstallOptions
|
|
163
|
-
): ComponentType[] | undefined {
|
|
164
|
-
const installAll = !options.skills && !options.commands && !options.hooks && !options.agents;
|
|
165
|
-
if (installAll) return undefined; // Default: all
|
|
166
|
-
|
|
167
|
-
const types: ComponentType[] = [];
|
|
168
|
-
if (options.skills) types.push('skills');
|
|
169
|
-
if (options.commands) types.push('commands');
|
|
170
|
-
if (options.hooks) types.push('hooks');
|
|
171
|
-
if (options.agents) types.push('agents');
|
|
172
|
-
return types;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Install a plugin from a source.
|
|
177
|
-
*/
|
|
178
|
-
export async function installPlugin(
|
|
179
|
-
source: string,
|
|
180
|
-
options: PluginInstallOptions
|
|
181
|
-
): Promise<InstallResult> {
|
|
182
|
-
const parsed = parseSource(source);
|
|
183
|
-
const verbose = options.verbose ?? false;
|
|
184
|
-
|
|
185
|
-
if (verbose) {
|
|
186
|
-
console.error(`\n[verbose] Parsed source:`);
|
|
187
|
-
console.error(` Owner: ${parsed.owner}`);
|
|
188
|
-
console.error(` Repo: ${parsed.repo}`);
|
|
189
|
-
console.error(` Branch: ${parsed.branch ?? '(default)'}`);
|
|
190
|
-
console.error(` Path: ${parsed.path ?? '(root)'}`);
|
|
191
|
-
console.error(` Clone URL: ${buildGitCloneUrl(parsed)}`);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Clone repo to temp directory
|
|
195
|
-
if (verbose) {
|
|
196
|
-
console.error(`\n[verbose] Cloning repository...`);
|
|
197
|
-
}
|
|
198
|
-
const repoDir = cloneRepo(parsed);
|
|
199
|
-
if (verbose) {
|
|
200
|
-
console.error(`[verbose] Cloned to: ${repoDir}`);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
try {
|
|
204
|
-
// Read manifest from cloned repo
|
|
205
|
-
const manifest = readPluginManifest(repoDir, parsed);
|
|
206
|
-
|
|
207
|
-
if (verbose) {
|
|
208
|
-
console.error(`\n[verbose] Plugin manifest:`);
|
|
209
|
-
console.error(` Name: ${manifest.name}`);
|
|
210
|
-
console.error(` Version: ${manifest.version}`);
|
|
211
|
-
if (manifest.description) {
|
|
212
|
-
console.error(` Description: ${manifest.description}`);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Discover available components
|
|
217
|
-
const discovered = discoverComponents(repoDir, parsed);
|
|
218
|
-
|
|
219
|
-
if (verbose) {
|
|
220
|
-
const totalDiscovered = discovered.skills.length + discovered.commands.length + discovered.hooks.length + discovered.agents.length;
|
|
221
|
-
console.error(`\n[verbose] Discovered ${totalDiscovered} component(s):`);
|
|
222
|
-
if (discovered.skills.length > 0) {
|
|
223
|
-
console.error(` Skills (${discovered.skills.length}): ${discovered.skills.join(', ')}`);
|
|
224
|
-
}
|
|
225
|
-
if (discovered.commands.length > 0) {
|
|
226
|
-
console.error(` Commands (${discovered.commands.length}): ${discovered.commands.join(', ')}`);
|
|
227
|
-
}
|
|
228
|
-
if (discovered.hooks.length > 0) {
|
|
229
|
-
console.error(` Hooks (${discovered.hooks.length}): ${discovered.hooks.join(', ')}`);
|
|
230
|
-
}
|
|
231
|
-
if (discovered.agents.length > 0) {
|
|
232
|
-
console.error(` Agents (${discovered.agents.length}): ${discovered.agents.join(', ')}`);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Determine what to install
|
|
237
|
-
const toInstall = determineComponentsToInstall(options, discovered);
|
|
238
|
-
|
|
239
|
-
if (verbose) {
|
|
240
|
-
const totalToInstall = toInstall.skills.length + toInstall.commands.length + toInstall.hooks.length + toInstall.agents.length;
|
|
241
|
-
console.error(`\n[verbose] Installing ${totalToInstall} component(s):`);
|
|
242
|
-
if (toInstall.skills.length > 0) {
|
|
243
|
-
console.error(` Skills: ${toInstall.skills.join(', ')}`);
|
|
244
|
-
}
|
|
245
|
-
if (toInstall.commands.length > 0) {
|
|
246
|
-
console.error(` Commands: ${toInstall.commands.join(', ')}`);
|
|
247
|
-
}
|
|
248
|
-
if (toInstall.hooks.length > 0) {
|
|
249
|
-
console.error(` Hooks: ${toInstall.hooks.join(', ')}`);
|
|
250
|
-
}
|
|
251
|
-
if (toInstall.agents.length > 0) {
|
|
252
|
-
console.error(` Agents: ${toInstall.agents.join(', ')}`);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const targetDir = options.targetDir || process.cwd();
|
|
257
|
-
const installed: InstalledComponent[] = [];
|
|
258
|
-
|
|
259
|
-
if (verbose) {
|
|
260
|
-
console.error(`\n[verbose] Target directory: ${targetDir}`);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Install skills
|
|
264
|
-
for (const skillName of toInstall.skills) {
|
|
265
|
-
const destPath = path.join(targetDir, '.claude', 'skills', skillName);
|
|
266
|
-
|
|
267
|
-
if (fs.existsSync(destPath)) {
|
|
268
|
-
if (!options.force) {
|
|
269
|
-
console.error(` Skipping existing skill: ${skillName}`);
|
|
270
|
-
if (verbose) {
|
|
271
|
-
console.error(` [verbose] Path: ${destPath}`);
|
|
272
|
-
}
|
|
273
|
-
continue;
|
|
274
|
-
}
|
|
275
|
-
if (verbose) {
|
|
276
|
-
console.error(` [verbose] Removing existing: ${destPath}`);
|
|
277
|
-
}
|
|
278
|
-
fs.rmSync(destPath, { recursive: true });
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
copyComponent(repoDir, parsed, 'skills', skillName, targetDir);
|
|
282
|
-
installed.push({ name: skillName, type: 'skills', path: `.claude/skills/${skillName}` });
|
|
283
|
-
console.error(` Installed skill: ${skillName}`);
|
|
284
|
-
if (verbose) {
|
|
285
|
-
console.error(` [verbose] Path: ${destPath}`);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Install commands
|
|
290
|
-
for (const cmdName of toInstall.commands) {
|
|
291
|
-
const destPath = path.join(targetDir, '.claude', 'commands', cmdName);
|
|
292
|
-
|
|
293
|
-
if (fs.existsSync(destPath)) {
|
|
294
|
-
if (!options.force) {
|
|
295
|
-
console.error(` Skipping existing command: ${cmdName}`);
|
|
296
|
-
if (verbose) {
|
|
297
|
-
console.error(` [verbose] Path: ${destPath}`);
|
|
298
|
-
}
|
|
299
|
-
continue;
|
|
300
|
-
}
|
|
301
|
-
if (verbose) {
|
|
302
|
-
console.error(` [verbose] Removing existing: ${destPath}`);
|
|
303
|
-
}
|
|
304
|
-
fs.rmSync(destPath, { recursive: true });
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
copyComponent(repoDir, parsed, 'commands', cmdName, targetDir);
|
|
308
|
-
installed.push({ name: cmdName, type: 'commands', path: `.claude/commands/${cmdName}` });
|
|
309
|
-
console.error(` Installed command: ${cmdName}`);
|
|
310
|
-
if (verbose) {
|
|
311
|
-
console.error(` [verbose] Path: ${destPath}`);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// Install hooks
|
|
316
|
-
for (const hookName of toInstall.hooks) {
|
|
317
|
-
const destPath = path.join(targetDir, '.claude', 'hooks', hookName);
|
|
318
|
-
|
|
319
|
-
if (fs.existsSync(destPath)) {
|
|
320
|
-
if (!options.force) {
|
|
321
|
-
console.error(` Skipping existing hook: ${hookName}`);
|
|
322
|
-
if (verbose) {
|
|
323
|
-
console.error(` [verbose] Path: ${destPath}`);
|
|
324
|
-
}
|
|
325
|
-
continue;
|
|
326
|
-
}
|
|
327
|
-
if (verbose) {
|
|
328
|
-
console.error(` [verbose] Removing existing: ${destPath}`);
|
|
329
|
-
}
|
|
330
|
-
fs.rmSync(destPath, { recursive: true });
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
copyComponent(repoDir, parsed, 'hooks', hookName, targetDir);
|
|
334
|
-
installed.push({ name: hookName, type: 'hooks', path: `.claude/hooks/${hookName}` });
|
|
335
|
-
console.error(` Installed hook: ${hookName}`);
|
|
336
|
-
if (verbose) {
|
|
337
|
-
console.error(` [verbose] Path: ${destPath}`);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// Install agents
|
|
342
|
-
for (const agentName of toInstall.agents) {
|
|
343
|
-
const destPath = path.join(targetDir, '.claude', 'agents', agentName);
|
|
344
|
-
|
|
345
|
-
if (fs.existsSync(destPath)) {
|
|
346
|
-
if (!options.force) {
|
|
347
|
-
console.error(` Skipping existing agent: ${agentName}`);
|
|
348
|
-
if (verbose) {
|
|
349
|
-
console.error(` [verbose] Path: ${destPath}`);
|
|
350
|
-
}
|
|
351
|
-
continue;
|
|
352
|
-
}
|
|
353
|
-
if (verbose) {
|
|
354
|
-
console.error(` [verbose] Removing existing: ${destPath}`);
|
|
355
|
-
}
|
|
356
|
-
fs.rmSync(destPath, { recursive: true });
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
copyComponent(repoDir, parsed, 'agents', agentName, targetDir);
|
|
360
|
-
installed.push({ name: agentName, type: 'agents', path: `.claude/agents/${agentName}` });
|
|
361
|
-
console.error(` Installed agent: ${agentName}`);
|
|
362
|
-
if (verbose) {
|
|
363
|
-
console.error(` [verbose] Path: ${destPath}`);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// Update agentio.json
|
|
368
|
-
if (verbose) {
|
|
369
|
-
console.error(`\n[verbose] Updating agentio.json`);
|
|
370
|
-
}
|
|
371
|
-
addPlugin(targetDir, manifest.name, {
|
|
372
|
-
source: source,
|
|
373
|
-
version: manifest.version,
|
|
374
|
-
components: getInstalledComponentTypes(options),
|
|
375
|
-
installedComponents: installed,
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
return {
|
|
379
|
-
success: true,
|
|
380
|
-
manifest,
|
|
381
|
-
installed,
|
|
382
|
-
};
|
|
383
|
-
} finally {
|
|
384
|
-
// Always cleanup temp directory
|
|
385
|
-
if (verbose) {
|
|
386
|
-
console.error(`\n[verbose] Cleaning up temp directory: ${repoDir}`);
|
|
387
|
-
}
|
|
388
|
-
cleanupTempDir(repoDir);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
* Remove installed components for a plugin.
|
|
394
|
-
*/
|
|
395
|
-
export function removePluginFiles(
|
|
396
|
-
targetDir: string,
|
|
397
|
-
components: InstalledComponent[]
|
|
398
|
-
): void {
|
|
399
|
-
for (const comp of components) {
|
|
400
|
-
const compPath = path.join(targetDir, comp.path);
|
|
401
|
-
if (fs.existsSync(compPath)) {
|
|
402
|
-
fs.rmSync(compPath, { recursive: true });
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
}
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { CliError } from '../../utils/errors';
|
|
2
|
-
import type { ParsedSource } from '../../types/claude-plugin';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Parse various GitHub source formats into a normalized ParsedSource.
|
|
6
|
-
*
|
|
7
|
-
* Supported formats:
|
|
8
|
-
* - https://github.com/owner/repo/tree/branch/path
|
|
9
|
-
* - git@github.com:owner/repo.git
|
|
10
|
-
* - https://github.com/owner/repo.git
|
|
11
|
-
* - owner/repo
|
|
12
|
-
* - owner/repo/path/to/plugin
|
|
13
|
-
*/
|
|
14
|
-
export function parseSource(source: string): ParsedSource {
|
|
15
|
-
// Full GitHub tree URL: https://github.com/owner/repo/tree/branch/path
|
|
16
|
-
if (source.includes('github.com') && source.includes('/tree/')) {
|
|
17
|
-
return parseGitHubTreeUrl(source);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// SSH URL: git@github.com:owner/repo.git
|
|
21
|
-
if (source.startsWith('git@')) {
|
|
22
|
-
return parseSshUrl(source);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// HTTPS clone URL: https://github.com/owner/repo.git
|
|
26
|
-
if (source.includes('github.com') && source.endsWith('.git')) {
|
|
27
|
-
return parseHttpsCloneUrl(source);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Plain GitHub URL without tree: https://github.com/owner/repo or https://github.com/owner/repo/path
|
|
31
|
-
if (source.includes('github.com')) {
|
|
32
|
-
return parseGitHubUrl(source);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Short form: owner/repo or owner/repo/path
|
|
36
|
-
return parseShortForm(source);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function parseGitHubTreeUrl(url: string): ParsedSource {
|
|
40
|
-
// https://github.com/owner/repo/tree/branch/path/to/plugin
|
|
41
|
-
const match = url.match(
|
|
42
|
-
/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)(?:\/(.+))?/
|
|
43
|
-
);
|
|
44
|
-
if (!match) {
|
|
45
|
-
throw new CliError('INVALID_PARAMS', `Invalid GitHub tree URL: ${url}`);
|
|
46
|
-
}
|
|
47
|
-
return {
|
|
48
|
-
owner: match[1],
|
|
49
|
-
repo: match[2],
|
|
50
|
-
branch: match[3],
|
|
51
|
-
path: match[4] || undefined,
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function parseSshUrl(url: string): ParsedSource {
|
|
56
|
-
// git@github.com:owner/repo.git
|
|
57
|
-
const match = url.match(/git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
58
|
-
if (!match) {
|
|
59
|
-
throw new CliError('INVALID_PARAMS', `Invalid SSH URL: ${url}`);
|
|
60
|
-
}
|
|
61
|
-
return {
|
|
62
|
-
owner: match[1],
|
|
63
|
-
repo: match[2],
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function parseHttpsCloneUrl(url: string): ParsedSource {
|
|
68
|
-
// https://github.com/owner/repo.git
|
|
69
|
-
const match = url.match(/github\.com\/([^/]+)\/([^/]+?)\.git$/);
|
|
70
|
-
if (!match) {
|
|
71
|
-
throw new CliError('INVALID_PARAMS', `Invalid HTTPS clone URL: ${url}`);
|
|
72
|
-
}
|
|
73
|
-
return {
|
|
74
|
-
owner: match[1],
|
|
75
|
-
repo: match[2],
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function parseGitHubUrl(url: string): ParsedSource {
|
|
80
|
-
// https://github.com/owner/repo or https://github.com/owner/repo/path
|
|
81
|
-
const match = url.match(/github\.com\/([^/]+)\/([^/]+?)(?:\/(.+))?$/);
|
|
82
|
-
if (!match) {
|
|
83
|
-
throw new CliError('INVALID_PARAMS', `Invalid GitHub URL: ${url}`);
|
|
84
|
-
}
|
|
85
|
-
return {
|
|
86
|
-
owner: match[1],
|
|
87
|
-
repo: match[2],
|
|
88
|
-
path: match[3] || undefined,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function parseShortForm(source: string): ParsedSource {
|
|
93
|
-
// owner/repo or owner/repo/path/to/plugin
|
|
94
|
-
const parts = source.split('/');
|
|
95
|
-
if (parts.length < 2) {
|
|
96
|
-
throw new CliError(
|
|
97
|
-
'INVALID_PARAMS',
|
|
98
|
-
`Invalid source format: ${source}`,
|
|
99
|
-
'Use owner/repo or a GitHub URL'
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
return {
|
|
103
|
-
owner: parts[0],
|
|
104
|
-
repo: parts[1],
|
|
105
|
-
path: parts.length > 2 ? parts.slice(2).join('/') : undefined,
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Build a git clone URL from parsed source.
|
|
111
|
-
*/
|
|
112
|
-
export function buildGitCloneUrl(parsed: ParsedSource): string {
|
|
113
|
-
return `https://github.com/${parsed.owner}/${parsed.repo}.git`;
|
|
114
|
-
}
|