@plosson/agentio 0.1.18 → 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -3
- package/package.json +1 -1
- package/src/commands/claude.ts +179 -0
- package/src/index.ts +2 -2
- package/src/services/claude-plugin/agentio-json.ts +103 -0
- package/src/services/claude-plugin/installer.ts +285 -0
- package/src/services/claude-plugin/source-parser.ts +114 -0
- package/src/types/claude-plugin.ts +62 -0
- package/src/commands/skill.ts +0 -204
package/README.md
CHANGED
|
@@ -67,10 +67,11 @@ Download from [GitHub Releases](https://github.com/plosson/agentio/releases/late
|
|
|
67
67
|
|
|
68
68
|
| Service | Status | Commands |
|
|
69
69
|
|---------|--------|----------|
|
|
70
|
-
| Gmail | Available | `list`, `get`, `search`, `send`, `reply`, `archive`, `mark` |
|
|
70
|
+
| Gmail | Available | `list`, `get`, `search`, `send`, `reply`, `archive`, `mark`, `attachment`, `export` |
|
|
71
71
|
| Telegram | Available | `send` |
|
|
72
|
-
|
|
|
73
|
-
|
|
|
72
|
+
| Google Chat | Available | `send`, `list`, `get` |
|
|
73
|
+
| Slack | Available | `send` |
|
|
74
|
+
| JIRA | Available | `projects`, `search`, `get`, `comment`, `transitions`, `transition` |
|
|
74
75
|
| Linear | Planned | - |
|
|
75
76
|
|
|
76
77
|
## Usage
|
|
@@ -95,6 +96,14 @@ agentio gmail send --to user@example.com --subject "Hello" --body "Message body"
|
|
|
95
96
|
|
|
96
97
|
# Or pipe content
|
|
97
98
|
echo "Message body" | agentio gmail send --to user@example.com --subject "Hello"
|
|
99
|
+
|
|
100
|
+
# Download attachments
|
|
101
|
+
agentio gmail attachment <message-id>
|
|
102
|
+
agentio gmail attachment <message-id> --name "document.pdf" --output ./downloads
|
|
103
|
+
|
|
104
|
+
# Export email as PDF
|
|
105
|
+
agentio gmail export <message-id>
|
|
106
|
+
agentio gmail export <message-id> --output email.pdf
|
|
98
107
|
```
|
|
99
108
|
|
|
100
109
|
### Telegram
|
|
@@ -110,6 +119,64 @@ agentio telegram send "Hello from agentio!"
|
|
|
110
119
|
agentio telegram send --parse-mode markdown "**Bold** and _italic_"
|
|
111
120
|
```
|
|
112
121
|
|
|
122
|
+
### Google Chat
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# Set up profile (webhook or OAuth)
|
|
126
|
+
agentio gchat profile add
|
|
127
|
+
|
|
128
|
+
# Send message via webhook
|
|
129
|
+
agentio gchat send "Hello from agentio!"
|
|
130
|
+
|
|
131
|
+
# Send with JSON payload for rich messages
|
|
132
|
+
agentio gchat send --json message.json
|
|
133
|
+
|
|
134
|
+
# List messages (OAuth profiles only)
|
|
135
|
+
agentio gchat list --space <space-id>
|
|
136
|
+
|
|
137
|
+
# Get a specific message (OAuth profiles only)
|
|
138
|
+
agentio gchat get <message-id> --space <space-id>
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Slack
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
# Set up webhook profile
|
|
145
|
+
agentio slack profile add
|
|
146
|
+
|
|
147
|
+
# Send message
|
|
148
|
+
agentio slack send "Hello from agentio!"
|
|
149
|
+
|
|
150
|
+
# Send Block Kit message from JSON file
|
|
151
|
+
agentio slack send --json blocks.json
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### JIRA
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
# Authenticate with OAuth
|
|
158
|
+
agentio jira profile add
|
|
159
|
+
|
|
160
|
+
# List projects
|
|
161
|
+
agentio jira projects
|
|
162
|
+
|
|
163
|
+
# Search issues
|
|
164
|
+
agentio jira search --project MYPROJ --status "In Progress"
|
|
165
|
+
agentio jira search --jql "assignee = currentUser() AND status != Done"
|
|
166
|
+
|
|
167
|
+
# Get issue details
|
|
168
|
+
agentio jira get PROJ-123
|
|
169
|
+
|
|
170
|
+
# Add a comment
|
|
171
|
+
agentio jira comment PROJ-123 "This is my comment"
|
|
172
|
+
|
|
173
|
+
# View available transitions
|
|
174
|
+
agentio jira transitions PROJ-123
|
|
175
|
+
|
|
176
|
+
# Transition an issue
|
|
177
|
+
agentio jira transition PROJ-123 <transition-id>
|
|
178
|
+
```
|
|
179
|
+
|
|
113
180
|
## Multi-Profile Support
|
|
114
181
|
|
|
115
182
|
Each service supports multiple named profiles:
|
|
@@ -180,6 +247,27 @@ Configuration is stored in `~/.config/agentio/`:
|
|
|
180
247
|
- `config.json` - Profile names and defaults
|
|
181
248
|
- `tokens.enc` - Encrypted credentials (AES-256-GCM)
|
|
182
249
|
|
|
250
|
+
### Export/Import
|
|
251
|
+
|
|
252
|
+
Transfer configuration between machines:
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
# Export configuration (generates encryption key)
|
|
256
|
+
agentio config export
|
|
257
|
+
|
|
258
|
+
# Export with custom output file
|
|
259
|
+
agentio config export --output backup.config
|
|
260
|
+
|
|
261
|
+
# Import on another machine
|
|
262
|
+
agentio config import agentio.config --key <encryption-key>
|
|
263
|
+
|
|
264
|
+
# Or use environment variable
|
|
265
|
+
AGENTIO_KEY=<key> agentio config import agentio.config
|
|
266
|
+
|
|
267
|
+
# Merge with existing config instead of replacing
|
|
268
|
+
agentio config import agentio.config --key <key> --merge
|
|
269
|
+
```
|
|
270
|
+
|
|
183
271
|
## License
|
|
184
272
|
|
|
185
273
|
MIT
|
package/package.json
CHANGED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import { CliError, handleError } from '../utils/errors';
|
|
5
|
+
import {
|
|
6
|
+
loadAgentioJson,
|
|
7
|
+
agentioJsonExists,
|
|
8
|
+
listPlugins,
|
|
9
|
+
removePlugin,
|
|
10
|
+
getPlugin,
|
|
11
|
+
} from '../services/claude-plugin/agentio-json';
|
|
12
|
+
import {
|
|
13
|
+
installPlugin,
|
|
14
|
+
removePluginFiles,
|
|
15
|
+
} from '../services/claude-plugin/installer';
|
|
16
|
+
import type { InstalledComponent } from '../types/claude-plugin';
|
|
17
|
+
|
|
18
|
+
export function registerClaudeCommands(program: Command): void {
|
|
19
|
+
const claude = program
|
|
20
|
+
.command('claude')
|
|
21
|
+
.description('Claude Code plugin operations');
|
|
22
|
+
|
|
23
|
+
const plugin = claude.command('plugin').description('Manage Claude Code plugins');
|
|
24
|
+
|
|
25
|
+
plugin
|
|
26
|
+
.command('install')
|
|
27
|
+
.description('Install plugin(s) from GitHub or agentio.json')
|
|
28
|
+
.argument('[source]', 'GitHub URL or owner/repo (omit to install from agentio.json)')
|
|
29
|
+
.option('--skills', 'Install only skills')
|
|
30
|
+
.option('--commands', 'Install only commands')
|
|
31
|
+
.option('--hooks', 'Install only hooks')
|
|
32
|
+
.option('-f, --force', 'Force reinstall if already exists')
|
|
33
|
+
.option('-d, --dir <path>', 'Target directory (default: current directory)')
|
|
34
|
+
.action(async (source, options) => {
|
|
35
|
+
try {
|
|
36
|
+
const targetDir = options.dir ? path.resolve(options.dir) : process.cwd();
|
|
37
|
+
|
|
38
|
+
if (!fs.existsSync(targetDir)) {
|
|
39
|
+
throw new CliError('INVALID_PARAMS', `Directory does not exist: ${targetDir}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (source) {
|
|
43
|
+
// Install specific plugin from source
|
|
44
|
+
console.error(`Installing plugin from: ${source}`);
|
|
45
|
+
console.error(`Target: ${path.join(targetDir, '.claude')}`);
|
|
46
|
+
|
|
47
|
+
const result = await installPlugin(source, {
|
|
48
|
+
skills: options.skills,
|
|
49
|
+
commands: options.commands,
|
|
50
|
+
hooks: options.hooks,
|
|
51
|
+
force: options.force,
|
|
52
|
+
targetDir,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
console.log(`\nInstalled: ${result.manifest.name} v${result.manifest.version}`);
|
|
56
|
+
if (result.installed.length > 0) {
|
|
57
|
+
console.log(`Components: ${result.installed.length}`);
|
|
58
|
+
for (const comp of result.installed) {
|
|
59
|
+
console.log(` ${comp.type}/${comp.name}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
// Install all plugins from agentio.json
|
|
64
|
+
if (!agentioJsonExists(targetDir)) {
|
|
65
|
+
throw new CliError(
|
|
66
|
+
'NOT_FOUND',
|
|
67
|
+
'No agentio.json found',
|
|
68
|
+
'Run: agentio claude plugin install <source> to install a plugin'
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const agentioJson = loadAgentioJson(targetDir);
|
|
73
|
+
const plugins = Object.entries(agentioJson.plugins);
|
|
74
|
+
|
|
75
|
+
if (plugins.length === 0) {
|
|
76
|
+
console.log('No plugins defined in agentio.json');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.error(`Installing ${plugins.length} plugin(s) from agentio.json...`);
|
|
81
|
+
console.error(`Target: ${path.join(targetDir, '.claude')}`);
|
|
82
|
+
|
|
83
|
+
let installed = 0;
|
|
84
|
+
for (const [name, entry] of plugins) {
|
|
85
|
+
console.error(`\nInstalling ${name}...`);
|
|
86
|
+
|
|
87
|
+
// Determine component flags based on entry.components
|
|
88
|
+
const installOptions = {
|
|
89
|
+
skills:
|
|
90
|
+
!entry.components || entry.components.includes('skills'),
|
|
91
|
+
commands:
|
|
92
|
+
!entry.components || entry.components.includes('commands'),
|
|
93
|
+
hooks: !entry.components || entry.components.includes('hooks'),
|
|
94
|
+
force: options.force,
|
|
95
|
+
targetDir,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const result = await installPlugin(entry.source, installOptions);
|
|
100
|
+
console.log(` Installed: ${result.manifest.name} v${result.manifest.version}`);
|
|
101
|
+
installed++;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error(` Failed to install ${name}: ${error}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
console.log(`\nInstalled ${installed} of ${plugins.length} plugin(s)`);
|
|
108
|
+
}
|
|
109
|
+
} catch (error) {
|
|
110
|
+
handleError(error);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
plugin
|
|
115
|
+
.command('list')
|
|
116
|
+
.description('List plugins from agentio.json')
|
|
117
|
+
.option('-d, --dir <path>', 'Directory with agentio.json (default: current directory)')
|
|
118
|
+
.action(async (options) => {
|
|
119
|
+
try {
|
|
120
|
+
const targetDir = options.dir ? path.resolve(options.dir) : process.cwd();
|
|
121
|
+
|
|
122
|
+
const plugins = listPlugins(targetDir);
|
|
123
|
+
|
|
124
|
+
if (plugins.length === 0) {
|
|
125
|
+
console.log('No plugins in agentio.json');
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
console.log(`Plugins (${plugins.length}):\n`);
|
|
130
|
+
for (const { name, entry } of plugins) {
|
|
131
|
+
console.log(`${name} v${entry.version}`);
|
|
132
|
+
console.log(` Source: ${entry.source}`);
|
|
133
|
+
if (entry.components) {
|
|
134
|
+
console.log(` Components: ${entry.components.join(', ')}`);
|
|
135
|
+
}
|
|
136
|
+
console.log('');
|
|
137
|
+
}
|
|
138
|
+
} catch (error) {
|
|
139
|
+
handleError(error);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
plugin
|
|
144
|
+
.command('remove')
|
|
145
|
+
.description('Remove an installed plugin')
|
|
146
|
+
.argument('<name>', 'Plugin name')
|
|
147
|
+
.option('-d, --dir <path>', 'Directory with agentio.json (default: current directory)')
|
|
148
|
+
.action(async (name, options) => {
|
|
149
|
+
try {
|
|
150
|
+
const targetDir = options.dir ? path.resolve(options.dir) : process.cwd();
|
|
151
|
+
|
|
152
|
+
const entry = getPlugin(targetDir, name);
|
|
153
|
+
if (!entry) {
|
|
154
|
+
throw new CliError('NOT_FOUND', `Plugin '${name}' not found in agentio.json`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.error(`Removing plugin: ${name}...`);
|
|
158
|
+
|
|
159
|
+
// Use stored installed components (no network calls needed)
|
|
160
|
+
const components: InstalledComponent[] = entry.installedComponents || [];
|
|
161
|
+
|
|
162
|
+
// Remove files
|
|
163
|
+
removePluginFiles(targetDir, components);
|
|
164
|
+
|
|
165
|
+
// Update agentio.json
|
|
166
|
+
removePlugin(targetDir, name);
|
|
167
|
+
|
|
168
|
+
console.log(`Removed: ${name}`);
|
|
169
|
+
if (components.length > 0) {
|
|
170
|
+
console.log(`Removed components: ${components.length}`);
|
|
171
|
+
for (const comp of components) {
|
|
172
|
+
console.log(` ${comp.type}/${comp.name}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
} catch (error) {
|
|
176
|
+
handleError(error);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { registerJiraCommands } from './commands/jira';
|
|
|
7
7
|
import { registerSlackCommands } from './commands/slack';
|
|
8
8
|
import { registerUpdateCommand } from './commands/update';
|
|
9
9
|
import { registerConfigCommands } from './commands/config';
|
|
10
|
-
import {
|
|
10
|
+
import { registerClaudeCommands } from './commands/claude';
|
|
11
11
|
|
|
12
12
|
declare const BUILD_VERSION: string | undefined;
|
|
13
13
|
|
|
@@ -33,6 +33,6 @@ registerJiraCommands(program);
|
|
|
33
33
|
registerSlackCommands(program);
|
|
34
34
|
registerUpdateCommand(program);
|
|
35
35
|
registerConfigCommands(program);
|
|
36
|
-
|
|
36
|
+
registerClaudeCommands(program);
|
|
37
37
|
|
|
38
38
|
program.parse();
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import type { AgentioJson, AgentioPluginEntry } from '../../types/claude-plugin';
|
|
4
|
+
|
|
5
|
+
const AGENTIO_JSON_FILE = 'agentio.json';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Get the path to agentio.json in the given directory.
|
|
9
|
+
*/
|
|
10
|
+
function getAgentioJsonPath(dir: string): string {
|
|
11
|
+
return path.join(dir, AGENTIO_JSON_FILE);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Load agentio.json from the given directory.
|
|
16
|
+
* Returns empty structure if file doesn't exist.
|
|
17
|
+
*/
|
|
18
|
+
export function loadAgentioJson(dir: string): AgentioJson {
|
|
19
|
+
const filePath = getAgentioJsonPath(dir);
|
|
20
|
+
|
|
21
|
+
if (!fs.existsSync(filePath)) {
|
|
22
|
+
return { plugins: {} };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
26
|
+
const data = JSON.parse(content) as AgentioJson;
|
|
27
|
+
|
|
28
|
+
// Ensure plugins object exists
|
|
29
|
+
if (!data.plugins) {
|
|
30
|
+
data.plugins = {};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return data;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Save agentio.json to the given directory.
|
|
38
|
+
*/
|
|
39
|
+
export function saveAgentioJson(dir: string, data: AgentioJson): void {
|
|
40
|
+
const filePath = getAgentioJsonPath(dir);
|
|
41
|
+
const content = JSON.stringify(data, null, 2) + '\n';
|
|
42
|
+
fs.writeFileSync(filePath, content);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check if agentio.json exists in the given directory.
|
|
47
|
+
*/
|
|
48
|
+
export function agentioJsonExists(dir: string): boolean {
|
|
49
|
+
return fs.existsSync(getAgentioJsonPath(dir));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Add or update a plugin entry in agentio.json.
|
|
54
|
+
*/
|
|
55
|
+
export function addPlugin(
|
|
56
|
+
dir: string,
|
|
57
|
+
name: string,
|
|
58
|
+
entry: AgentioPluginEntry
|
|
59
|
+
): void {
|
|
60
|
+
const data = loadAgentioJson(dir);
|
|
61
|
+
data.plugins[name] = entry;
|
|
62
|
+
saveAgentioJson(dir, data);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Remove a plugin entry from agentio.json.
|
|
67
|
+
* Returns true if plugin was found and removed.
|
|
68
|
+
*/
|
|
69
|
+
export function removePlugin(dir: string, name: string): boolean {
|
|
70
|
+
const data = loadAgentioJson(dir);
|
|
71
|
+
|
|
72
|
+
if (!data.plugins[name]) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
delete data.plugins[name];
|
|
77
|
+
saveAgentioJson(dir, data);
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get a plugin entry from agentio.json.
|
|
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.
|
|
94
|
+
*/
|
|
95
|
+
export function listPlugins(
|
|
96
|
+
dir: string
|
|
97
|
+
): Array<{ name: string; entry: AgentioPluginEntry }> {
|
|
98
|
+
const data = loadAgentioJson(dir);
|
|
99
|
+
return Object.entries(data.plugins).map(([name, entry]) => ({
|
|
100
|
+
name,
|
|
101
|
+
entry,
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
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
|
+
};
|
|
105
|
+
|
|
106
|
+
const componentTypes: ComponentType[] = ['skills', 'commands', 'hooks'];
|
|
107
|
+
|
|
108
|
+
for (const type of componentTypes) {
|
|
109
|
+
const typePath = path.join(basePath, type);
|
|
110
|
+
if (fs.existsSync(typePath)) {
|
|
111
|
+
const entries = fs.readdirSync(typePath, { withFileTypes: true });
|
|
112
|
+
result[type] = entries
|
|
113
|
+
.filter((e) => e.isDirectory())
|
|
114
|
+
.map((e) => e.name);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Copy a component from the cloned repository to the target directory.
|
|
123
|
+
*/
|
|
124
|
+
function copyComponent(
|
|
125
|
+
repoDir: string,
|
|
126
|
+
parsed: ParsedSource,
|
|
127
|
+
componentType: ComponentType,
|
|
128
|
+
componentName: string,
|
|
129
|
+
targetDir: string
|
|
130
|
+
): void {
|
|
131
|
+
const basePath = parsed.path ? path.join(repoDir, parsed.path) : repoDir;
|
|
132
|
+
const srcPath = path.join(basePath, componentType, componentName);
|
|
133
|
+
const destPath = path.join(targetDir, '.claude', componentType, componentName);
|
|
134
|
+
|
|
135
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
136
|
+
fs.cpSync(srcPath, destPath, { recursive: true });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Determine which components to install based on options.
|
|
141
|
+
*/
|
|
142
|
+
function determineComponentsToInstall(
|
|
143
|
+
options: PluginInstallOptions,
|
|
144
|
+
discovered: DiscoveredComponents
|
|
145
|
+
): DiscoveredComponents {
|
|
146
|
+
// If no specific flags, install all
|
|
147
|
+
const installAll = !options.skills && !options.commands && !options.hooks;
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
skills: installAll || options.skills ? discovered.skills : [],
|
|
151
|
+
commands: installAll || options.commands ? discovered.commands : [],
|
|
152
|
+
hooks: installAll || options.hooks ? discovered.hooks : [],
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get the component types array for agentio.json based on what was installed.
|
|
158
|
+
*/
|
|
159
|
+
function getInstalledComponentTypes(
|
|
160
|
+
options: PluginInstallOptions
|
|
161
|
+
): ComponentType[] | undefined {
|
|
162
|
+
const installAll = !options.skills && !options.commands && !options.hooks;
|
|
163
|
+
if (installAll) return undefined; // Default: all
|
|
164
|
+
|
|
165
|
+
const types: ComponentType[] = [];
|
|
166
|
+
if (options.skills) types.push('skills');
|
|
167
|
+
if (options.commands) types.push('commands');
|
|
168
|
+
if (options.hooks) types.push('hooks');
|
|
169
|
+
return types;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Install a plugin from a source.
|
|
174
|
+
*/
|
|
175
|
+
export async function installPlugin(
|
|
176
|
+
source: string,
|
|
177
|
+
options: PluginInstallOptions
|
|
178
|
+
): Promise<InstallResult> {
|
|
179
|
+
const parsed = parseSource(source);
|
|
180
|
+
|
|
181
|
+
// Clone repo to temp directory
|
|
182
|
+
const repoDir = cloneRepo(parsed);
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
// Read manifest from cloned repo
|
|
186
|
+
const manifest = readPluginManifest(repoDir, parsed);
|
|
187
|
+
|
|
188
|
+
// Discover available components
|
|
189
|
+
const discovered = discoverComponents(repoDir, parsed);
|
|
190
|
+
|
|
191
|
+
// Determine what to install
|
|
192
|
+
const toInstall = determineComponentsToInstall(options, discovered);
|
|
193
|
+
|
|
194
|
+
const targetDir = options.targetDir || process.cwd();
|
|
195
|
+
const installed: InstalledComponent[] = [];
|
|
196
|
+
|
|
197
|
+
// Install skills
|
|
198
|
+
for (const skillName of toInstall.skills) {
|
|
199
|
+
const destPath = path.join(targetDir, '.claude', 'skills', skillName);
|
|
200
|
+
|
|
201
|
+
if (fs.existsSync(destPath)) {
|
|
202
|
+
if (!options.force) {
|
|
203
|
+
console.error(` Skipping existing skill: ${skillName}`);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
fs.rmSync(destPath, { recursive: true });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
copyComponent(repoDir, parsed, 'skills', skillName, targetDir);
|
|
210
|
+
installed.push({ name: skillName, type: 'skills', path: destPath });
|
|
211
|
+
console.error(` Installed skill: ${skillName}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Install commands
|
|
215
|
+
for (const cmdName of toInstall.commands) {
|
|
216
|
+
const destPath = path.join(targetDir, '.claude', 'commands', cmdName);
|
|
217
|
+
|
|
218
|
+
if (fs.existsSync(destPath)) {
|
|
219
|
+
if (!options.force) {
|
|
220
|
+
console.error(` Skipping existing command: ${cmdName}`);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
fs.rmSync(destPath, { recursive: true });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
copyComponent(repoDir, parsed, 'commands', cmdName, targetDir);
|
|
227
|
+
installed.push({ name: cmdName, type: 'commands', path: destPath });
|
|
228
|
+
console.error(` Installed command: ${cmdName}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Install hooks
|
|
232
|
+
for (const hookName of toInstall.hooks) {
|
|
233
|
+
const destPath = path.join(targetDir, '.claude', 'hooks', hookName);
|
|
234
|
+
|
|
235
|
+
if (fs.existsSync(destPath)) {
|
|
236
|
+
if (!options.force) {
|
|
237
|
+
console.error(` Skipping existing hook: ${hookName}`);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
fs.rmSync(destPath, { recursive: true });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
copyComponent(repoDir, parsed, 'hooks', hookName, targetDir);
|
|
244
|
+
installed.push({ name: hookName, type: 'hooks', path: destPath });
|
|
245
|
+
console.error(` Installed hook: ${hookName}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Update agentio.json
|
|
249
|
+
addPlugin(targetDir, manifest.name, {
|
|
250
|
+
source: source,
|
|
251
|
+
version: manifest.version,
|
|
252
|
+
components: getInstalledComponentTypes(options),
|
|
253
|
+
installedComponents: installed,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
success: true,
|
|
258
|
+
manifest,
|
|
259
|
+
installed,
|
|
260
|
+
};
|
|
261
|
+
} finally {
|
|
262
|
+
// Always cleanup temp directory
|
|
263
|
+
cleanupTempDir(repoDir);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Remove installed components for a plugin.
|
|
269
|
+
*/
|
|
270
|
+
export function removePluginFiles(
|
|
271
|
+
targetDir: string,
|
|
272
|
+
components: InstalledComponent[]
|
|
273
|
+
): void {
|
|
274
|
+
for (const comp of components) {
|
|
275
|
+
const compPath = path.join(
|
|
276
|
+
targetDir,
|
|
277
|
+
'.claude',
|
|
278
|
+
comp.type,
|
|
279
|
+
comp.name
|
|
280
|
+
);
|
|
281
|
+
if (fs.existsSync(compPath)) {
|
|
282
|
+
fs.rmSync(compPath, { recursive: true });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Source URL parsing result
|
|
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';
|
|
18
|
+
|
|
19
|
+
// Discovered components from filesystem
|
|
20
|
+
export interface DiscoveredComponents {
|
|
21
|
+
skills: string[];
|
|
22
|
+
commands: string[];
|
|
23
|
+
hooks: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Installation options
|
|
27
|
+
export interface PluginInstallOptions {
|
|
28
|
+
skills?: boolean;
|
|
29
|
+
commands?: boolean;
|
|
30
|
+
hooks?: boolean;
|
|
31
|
+
force?: boolean;
|
|
32
|
+
targetDir?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Single installed component record
|
|
36
|
+
export interface InstalledComponent {
|
|
37
|
+
name: string;
|
|
38
|
+
type: ComponentType;
|
|
39
|
+
path: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Plugin entry in agentio.json
|
|
43
|
+
export interface AgentioPluginEntry {
|
|
44
|
+
source: string;
|
|
45
|
+
version: string;
|
|
46
|
+
components?: ComponentType[];
|
|
47
|
+
installedComponents: InstalledComponent[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// agentio.json structure
|
|
51
|
+
export interface AgentioJson {
|
|
52
|
+
plugins: {
|
|
53
|
+
[pluginName: string]: AgentioPluginEntry;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Installation result
|
|
58
|
+
export interface InstallResult {
|
|
59
|
+
success: boolean;
|
|
60
|
+
manifest: PluginManifest;
|
|
61
|
+
installed: InstalledComponent[];
|
|
62
|
+
}
|
package/src/commands/skill.ts
DELETED
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import { createInterface } from 'readline';
|
|
3
|
-
import { CliError, handleError } from '../utils/errors';
|
|
4
|
-
import * as fs from 'fs';
|
|
5
|
-
import * as path from 'path';
|
|
6
|
-
|
|
7
|
-
const GITHUB_REPO = 'plosson/agentio';
|
|
8
|
-
const SKILLS_PATH = 'claude/skills';
|
|
9
|
-
|
|
10
|
-
interface GitHubContent {
|
|
11
|
-
name: string;
|
|
12
|
-
path: string;
|
|
13
|
-
type: 'file' | 'dir';
|
|
14
|
-
download_url: string | null;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function prompt(question: string): Promise<string> {
|
|
18
|
-
const rl = createInterface({
|
|
19
|
-
input: process.stdin,
|
|
20
|
-
output: process.stderr,
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
return new Promise((resolve) => {
|
|
24
|
-
rl.question(question, (answer) => {
|
|
25
|
-
rl.close();
|
|
26
|
-
resolve(answer.trim());
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
async function fetchGitHubContents(repoPath: string): Promise<GitHubContent[]> {
|
|
32
|
-
const url = `https://api.github.com/repos/${GITHUB_REPO}/contents/${repoPath}`;
|
|
33
|
-
const response = await fetch(url, {
|
|
34
|
-
headers: {
|
|
35
|
-
'Accept': 'application/vnd.github.v3+json',
|
|
36
|
-
'User-Agent': 'agentio-skill-manager',
|
|
37
|
-
},
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
if (!response.ok) {
|
|
41
|
-
if (response.status === 404) {
|
|
42
|
-
throw new CliError('NOT_FOUND', `Path not found: ${repoPath}`);
|
|
43
|
-
}
|
|
44
|
-
throw new CliError('API_ERROR', `GitHub API error: ${response.statusText}`);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return response.json();
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function fetchFileContent(downloadUrl: string): Promise<string> {
|
|
51
|
-
const response = await fetch(downloadUrl, {
|
|
52
|
-
headers: {
|
|
53
|
-
'User-Agent': 'agentio-skill-manager',
|
|
54
|
-
},
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
if (!response.ok) {
|
|
58
|
-
throw new CliError('API_ERROR', `Failed to download file: ${response.statusText}`);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return response.text();
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async function listAvailableSkills(): Promise<string[]> {
|
|
65
|
-
const contents = await fetchGitHubContents(SKILLS_PATH);
|
|
66
|
-
return contents
|
|
67
|
-
.filter((item) => item.type === 'dir')
|
|
68
|
-
.map((item) => item.name);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async function downloadSkillFolder(
|
|
72
|
-
skillName: string,
|
|
73
|
-
targetDir: string
|
|
74
|
-
): Promise<void> {
|
|
75
|
-
const skillPath = `${SKILLS_PATH}/${skillName}`;
|
|
76
|
-
const contents = await fetchGitHubContents(skillPath);
|
|
77
|
-
|
|
78
|
-
// Create target directory
|
|
79
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
80
|
-
|
|
81
|
-
for (const item of contents) {
|
|
82
|
-
const targetPath = path.join(targetDir, item.name);
|
|
83
|
-
|
|
84
|
-
if (item.type === 'file' && item.download_url) {
|
|
85
|
-
const content = await fetchFileContent(item.download_url);
|
|
86
|
-
fs.writeFileSync(targetPath, content);
|
|
87
|
-
} else if (item.type === 'dir') {
|
|
88
|
-
// Recursively download subdirectories
|
|
89
|
-
await downloadSkillFolder(`${skillName}/${item.name}`, targetPath);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async function installSkill(
|
|
95
|
-
skillName: string,
|
|
96
|
-
baseDir: string,
|
|
97
|
-
skipPrompt: boolean
|
|
98
|
-
): Promise<boolean> {
|
|
99
|
-
const targetDir = path.join(baseDir, '.claude', 'skills', skillName);
|
|
100
|
-
|
|
101
|
-
// Check if skill already exists
|
|
102
|
-
if (fs.existsSync(targetDir)) {
|
|
103
|
-
if (!skipPrompt) {
|
|
104
|
-
const answer = await prompt(
|
|
105
|
-
`Skill '${skillName}' already exists at ${targetDir}. Update? [y/N] `
|
|
106
|
-
);
|
|
107
|
-
if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
|
|
108
|
-
console.error(`Skipping '${skillName}'`);
|
|
109
|
-
return false;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
// Remove existing skill directory before updating
|
|
113
|
-
fs.rmSync(targetDir, { recursive: true });
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
console.error(`Installing skill: ${skillName}...`);
|
|
117
|
-
await downloadSkillFolder(skillName, targetDir);
|
|
118
|
-
console.log(`Installed: ${skillName} -> ${targetDir}`);
|
|
119
|
-
return true;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export function registerSkillCommands(program: Command): void {
|
|
123
|
-
const skill = program
|
|
124
|
-
.command('skill')
|
|
125
|
-
.description('Manage Claude Code skills');
|
|
126
|
-
|
|
127
|
-
skill
|
|
128
|
-
.command('list')
|
|
129
|
-
.description('List available skills from the repository')
|
|
130
|
-
.action(async () => {
|
|
131
|
-
try {
|
|
132
|
-
console.error('Fetching available skills...');
|
|
133
|
-
const skills = await listAvailableSkills();
|
|
134
|
-
|
|
135
|
-
if (skills.length === 0) {
|
|
136
|
-
console.log('No skills found in repository');
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
console.log('Available skills:');
|
|
141
|
-
for (const name of skills) {
|
|
142
|
-
console.log(` ${name}`);
|
|
143
|
-
}
|
|
144
|
-
} catch (error) {
|
|
145
|
-
handleError(error);
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
skill
|
|
150
|
-
.command('install')
|
|
151
|
-
.description('Install skills from the repository')
|
|
152
|
-
.argument('[skill-name]', 'Name of the skill to install (omit to install all)')
|
|
153
|
-
.option('-d, --dir <path>', 'Target directory (default: current directory)')
|
|
154
|
-
.option('-y, --yes', 'Skip confirmation prompts')
|
|
155
|
-
.action(async (skillName, options) => {
|
|
156
|
-
try {
|
|
157
|
-
const baseDir = options.dir ? path.resolve(options.dir) : process.cwd();
|
|
158
|
-
|
|
159
|
-
// Verify base directory exists
|
|
160
|
-
if (!fs.existsSync(baseDir)) {
|
|
161
|
-
throw new CliError(
|
|
162
|
-
'INVALID_PARAMS',
|
|
163
|
-
`Directory does not exist: ${baseDir}`
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
console.error(`Target: ${path.join(baseDir, '.claude', 'skills')}`);
|
|
168
|
-
|
|
169
|
-
if (skillName) {
|
|
170
|
-
// Install specific skill
|
|
171
|
-
const skills = await listAvailableSkills();
|
|
172
|
-
if (!skills.includes(skillName)) {
|
|
173
|
-
throw new CliError(
|
|
174
|
-
'NOT_FOUND',
|
|
175
|
-
`Skill '${skillName}' not found`,
|
|
176
|
-
`Available skills: ${skills.join(', ')}`
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
await installSkill(skillName, baseDir, options.yes);
|
|
180
|
-
} else {
|
|
181
|
-
// Install all skills
|
|
182
|
-
console.error('Fetching available skills...');
|
|
183
|
-
const skills = await listAvailableSkills();
|
|
184
|
-
|
|
185
|
-
if (skills.length === 0) {
|
|
186
|
-
console.log('No skills found in repository');
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
console.error(`Found ${skills.length} skill(s)`);
|
|
191
|
-
|
|
192
|
-
let installed = 0;
|
|
193
|
-
for (const name of skills) {
|
|
194
|
-
const success = await installSkill(name, baseDir, options.yes);
|
|
195
|
-
if (success) installed++;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
console.log(`\nInstalled ${installed} of ${skills.length} skill(s)`);
|
|
199
|
-
}
|
|
200
|
-
} catch (error) {
|
|
201
|
-
handleError(error);
|
|
202
|
-
}
|
|
203
|
-
});
|
|
204
|
-
}
|