@plosson/agentio 0.1.22 → 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/README.md CHANGED
@@ -206,11 +206,15 @@ agentio claude plugin install https://github.com/plosson/agentio
206
206
  # Install to a specific directory
207
207
  agentio claude plugin install plosson/agentio -d ~/myproject
208
208
 
209
- # Install only skills (skip commands and hooks)
209
+ # Install only specific components (skills, commands, hooks, agents)
210
210
  agentio claude plugin install plosson/agentio --skills
211
+ agentio claude plugin install plosson/agentio --agents
211
212
 
212
213
  # Force reinstall if already exists
213
214
  agentio claude plugin install plosson/agentio -f
215
+
216
+ # Show detailed installation logs
217
+ agentio claude plugin install plosson/agentio --verbose
214
218
  ```
215
219
 
216
220
  Once installed, Claude Code can use the agentio CLI skills to help you manage emails, send Telegram messages, and more.
@@ -234,7 +238,7 @@ If your project has an `agentio.json` file listing plugins, you can install all
234
238
  agentio claude plugin install
235
239
  ```
236
240
 
237
- Plugins are installed to `.claude/` in the target directory (skills, commands, and hooks subdirectories).
241
+ Plugins are installed to `.claude/` in the target directory (skills, commands, hooks, and agents subdirectories).
238
242
 
239
243
  ## Design
240
244
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plosson/agentio",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "description": "CLI for LLM agents to interact with communication and tracking services",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,183 +1,267 @@
1
1
  import { Command } from 'commander';
2
2
  import * as path from 'path';
3
- import * as fs from 'fs';
3
+ import { spawn } from 'child_process';
4
4
  import { CliError, handleError } from '../utils/errors';
5
5
  import {
6
6
  loadAgentioJson,
7
- agentioJsonExists,
8
- listPlugins,
7
+ addMarketplace,
8
+ addPlugin,
9
+ removeMarketplace,
9
10
  removePlugin,
10
- getPlugin,
11
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';
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
- 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('--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
- if (!fs.existsSync(targetDir)) {
41
- throw new CliError('INVALID_PARAMS', `Directory does not exist: ${targetDir}`);
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
- if (source) {
45
- // Install specific plugin from source
46
- console.error(`Installing plugin from: ${source}`);
47
- console.error(`Target: ${path.join(targetDir, '.claude')}`);
48
-
49
- const result = await installPlugin(source, {
50
- skills: options.skills,
51
- commands: options.commands,
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
- console.log(`\nInstalled ${installed} of ${plugins.length} plugin(s)`);
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
- plugin
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
- const plugins = listPlugins(targetDir);
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
- console.log(`Plugins (${plugins.length}):\n`);
136
- for (const { name, entry } of plugins) {
137
- console.log(`${name} v${entry.version}`);
138
- console.log(` Source: ${entry.source}`);
139
- if (entry.components) {
140
- console.log(` Components: ${entry.components.join(', ')}`);
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
- plugin
150
- .command('remove')
151
- .description('Remove an installed plugin')
152
- .argument('<name>', 'Plugin name')
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 (name, options) => {
239
+ .action(async (url, options) => {
155
240
  try {
156
241
  const targetDir = options.dir ? path.resolve(options.dir) : process.cwd();
157
242
 
158
- const entry = getPlugin(targetDir, name);
159
- if (!entry) {
160
- throw new CliError('NOT_FOUND', `Plugin '${name}' not found in agentio.json`);
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
- console.error(`Removing plugin: ${name}...`);
164
-
165
- // Use stored installed components (no network calls needed)
166
- const components: InstalledComponent[] = entry.installedComponents || [];
167
-
168
- // Remove files
169
- removePluginFiles(targetDir, components);
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
- // Update agentio.json
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, AgentioPluginEntry } from '../../types/claude-plugin';
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 plugins object exists
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
- * Check if agentio.json exists in the given directory.
49
+ * Add a marketplace URL if not already present.
47
50
  */
48
- export function agentioJsonExists(dir: string): boolean {
49
- return fs.existsSync(getAgentioJsonPath(dir));
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 or update a plugin entry in agentio.json.
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[name] = entry;
62
- saveAgentioJson(dir, data);
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 plugin entry from agentio.json.
67
- * Returns true if plugin was found and removed.
71
+ * Remove a marketplace URL.
72
+ * Returns true if found and removed.
68
73
  */
69
- export function removePlugin(dir: string, name: string): boolean {
74
+ export function removeMarketplace(dir: string, url: string): boolean {
70
75
  const data = loadAgentioJson(dir);
71
-
72
- if (!data.plugins[name]) {
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
- * 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.
86
+ * Remove a plugin name.
87
+ * Returns true if found and removed.
94
88
  */
95
- export function listPlugins(
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
- return Object.entries(data.plugins).map(([name, entry]) => ({
100
- name,
101
- entry,
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
- // 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' | '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;
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
- plugins: {
56
- [pluginName: string]: AgentioPluginEntry;
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,410 +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: destPath });
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: destPath });
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: destPath });
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: destPath });
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(
401
- targetDir,
402
- '.claude',
403
- comp.type,
404
- comp.name
405
- );
406
- if (fs.existsSync(compPath)) {
407
- fs.rmSync(compPath, { recursive: true });
408
- }
409
- }
410
- }
@@ -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
- }