skillfish 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +17 -0
- package/README.md +192 -0
- package/dist/commands/add.d.ts +5 -0
- package/dist/commands/add.js +477 -0
- package/dist/commands/list.d.ts +5 -0
- package/dist/commands/list.js +278 -0
- package/dist/commands/remove.d.ts +5 -0
- package/dist/commands/remove.js +336 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +56 -0
- package/dist/lib/agents.d.ts +40 -0
- package/dist/lib/agents.js +146 -0
- package/dist/lib/constants.d.ts +65 -0
- package/dist/lib/constants.js +68 -0
- package/dist/lib/github.d.ts +52 -0
- package/dist/lib/github.js +185 -0
- package/dist/lib/installer.d.ts +64 -0
- package/dist/lib/installer.js +163 -0
- package/dist/telemetry.d.ts +10 -0
- package/dist/telemetry.js +27 -0
- package/dist/utils.d.ts +130 -0
- package/dist/utils.js +163 -0
- package/package.json +68 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `skillfish list` command - List installed skills.
|
|
3
|
+
*/
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
import { homedir } from 'os';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import * as p from '@clack/prompts';
|
|
8
|
+
import pc from 'picocolors';
|
|
9
|
+
import { getDetectedAgents, getAgentSkillDir } from '../lib/agents.js';
|
|
10
|
+
import { listInstalledSkillsInDir } from '../lib/installer.js';
|
|
11
|
+
import { EXIT_CODES } from '../lib/constants.js';
|
|
12
|
+
import { isInputTTY } from '../utils.js';
|
|
13
|
+
export const listCommand = new Command('list')
|
|
14
|
+
.description('List installed skills across all detected agents')
|
|
15
|
+
.option('--project', 'List project-level skills only (./.claude)')
|
|
16
|
+
.option('--global', 'List global skills only (~/.claude)')
|
|
17
|
+
.option('--agent <name>', 'Filter to a specific agent')
|
|
18
|
+
.helpOption('-h, --help', 'Display help for command')
|
|
19
|
+
.addHelpText('after', `
|
|
20
|
+
Examples:
|
|
21
|
+
$ skillfish list List all installed skills
|
|
22
|
+
$ skillfish list --agent "Claude Code" List skills for a specific agent
|
|
23
|
+
$ skillfish list --project List skills in current project
|
|
24
|
+
$ skillfish list --global List global skills only`)
|
|
25
|
+
.action(async (options, command) => {
|
|
26
|
+
const jsonMode = command.parent?.opts().json ?? false;
|
|
27
|
+
const projectFlag = options.project ?? false;
|
|
28
|
+
const globalFlag = options.global ?? false;
|
|
29
|
+
const agentFilter = options.agent;
|
|
30
|
+
// JSON output state (typed as ListJsonOutput)
|
|
31
|
+
const jsonOutput = {
|
|
32
|
+
success: true,
|
|
33
|
+
errors: [],
|
|
34
|
+
};
|
|
35
|
+
function addError(message) {
|
|
36
|
+
jsonOutput.errors.push(message);
|
|
37
|
+
jsonOutput.success = false;
|
|
38
|
+
}
|
|
39
|
+
function outputJsonAndExit(exitCode, data = {}) {
|
|
40
|
+
const output = {
|
|
41
|
+
success: jsonOutput.success,
|
|
42
|
+
exit_code: exitCode,
|
|
43
|
+
errors: jsonOutput.errors,
|
|
44
|
+
installed: data.installed ?? [],
|
|
45
|
+
agents_detected: data.agents_detected ?? [],
|
|
46
|
+
};
|
|
47
|
+
console.log(JSON.stringify(output, null, 2));
|
|
48
|
+
process.exit(exitCode);
|
|
49
|
+
}
|
|
50
|
+
function exitWithError(message, exitCode, data = {}) {
|
|
51
|
+
if (jsonMode) {
|
|
52
|
+
addError(message);
|
|
53
|
+
outputJsonAndExit(exitCode, data);
|
|
54
|
+
}
|
|
55
|
+
p.log.error(message);
|
|
56
|
+
process.exit(exitCode);
|
|
57
|
+
}
|
|
58
|
+
// Determine which locations to check
|
|
59
|
+
// By default, check both global and project. Flags narrow it down.
|
|
60
|
+
const checkGlobal = !projectFlag; // Check global unless --project is set
|
|
61
|
+
const checkProject = !globalFlag; // Check project unless --global is set
|
|
62
|
+
// Detect agents
|
|
63
|
+
const detected = getDetectedAgents();
|
|
64
|
+
if (detected.length === 0) {
|
|
65
|
+
exitWithError('No agents detected. Install Claude Code, Cursor, or another supported agent first.', EXIT_CODES.GENERAL_ERROR, { installed: [], agents_detected: [] });
|
|
66
|
+
}
|
|
67
|
+
// Helper to collect skills for given agents
|
|
68
|
+
function collectSkills(agents) {
|
|
69
|
+
const installed = [];
|
|
70
|
+
const globalSkills = [];
|
|
71
|
+
const projectSkills = [];
|
|
72
|
+
const seenPaths = new Set();
|
|
73
|
+
for (const agent of agents) {
|
|
74
|
+
if (checkGlobal) {
|
|
75
|
+
const globalDir = getAgentSkillDir(agent, homedir());
|
|
76
|
+
const skills = listInstalledSkillsInDir(globalDir);
|
|
77
|
+
for (const skill of skills) {
|
|
78
|
+
const skillPath = join(globalDir, skill);
|
|
79
|
+
if (!seenPaths.has(skillPath)) {
|
|
80
|
+
seenPaths.add(skillPath);
|
|
81
|
+
const item = { agent: agent.name, skill, path: skillPath, location: 'global' };
|
|
82
|
+
installed.push(item);
|
|
83
|
+
globalSkills.push(item);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (checkProject) {
|
|
88
|
+
const projectDir = getAgentSkillDir(agent, process.cwd());
|
|
89
|
+
const skills = listInstalledSkillsInDir(projectDir);
|
|
90
|
+
for (const skill of skills) {
|
|
91
|
+
const skillPath = join(projectDir, skill);
|
|
92
|
+
// Skip if already seen (avoids duplicates when cwd is under home)
|
|
93
|
+
if (!seenPaths.has(skillPath)) {
|
|
94
|
+
seenPaths.add(skillPath);
|
|
95
|
+
const item = { agent: agent.name, skill, path: skillPath, location: 'project' };
|
|
96
|
+
installed.push(item);
|
|
97
|
+
projectSkills.push(item);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return { installed, globalSkills, projectSkills };
|
|
103
|
+
}
|
|
104
|
+
// Helper to display skills for a single location
|
|
105
|
+
function displaySkillsForLocation(skills, locationLabel) {
|
|
106
|
+
console.log();
|
|
107
|
+
console.log(pc.bold(pc.underline(locationLabel)));
|
|
108
|
+
for (const item of skills) {
|
|
109
|
+
console.log(` ${pc.green('•')} ${item.skill}`);
|
|
110
|
+
}
|
|
111
|
+
console.log();
|
|
112
|
+
p.outro(`${pc.cyan(skills.length.toString())} skill${skills.length === 1 ? '' : 's'}`);
|
|
113
|
+
}
|
|
114
|
+
// Filter to specific agent if --agent flag provided
|
|
115
|
+
if (agentFilter) {
|
|
116
|
+
const found = detected.filter((a) => a.name.toLowerCase() === agentFilter.toLowerCase());
|
|
117
|
+
if (found.length === 0) {
|
|
118
|
+
exitWithError(`Agent "${agentFilter}" not found. Detected: ${detected.map((a) => a.name).join(', ')}`, EXIT_CODES.NOT_FOUND, { installed: [], agents_detected: detected.map((a) => a.name) });
|
|
119
|
+
}
|
|
120
|
+
const { installed, globalSkills, projectSkills } = collectSkills(found);
|
|
121
|
+
if (jsonMode) {
|
|
122
|
+
outputJsonAndExit(EXIT_CODES.SUCCESS, {
|
|
123
|
+
installed,
|
|
124
|
+
agents_detected: detected.map((a) => a.name),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
// Display intro
|
|
128
|
+
console.log();
|
|
129
|
+
p.intro(`${pc.bgCyan(pc.black(' skillfish '))} ${pc.dim(`Skills for ${found[0].name}`)}`);
|
|
130
|
+
if (globalSkills.length === 0 && projectSkills.length === 0) {
|
|
131
|
+
p.outro(pc.dim('No skills installed'));
|
|
132
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
133
|
+
}
|
|
134
|
+
// If both locations have skills, show location selector (same as interactive)
|
|
135
|
+
const hasBothLocations = globalSkills.length > 0 && projectSkills.length > 0;
|
|
136
|
+
if (hasBothLocations && isInputTTY()) {
|
|
137
|
+
const locationOptions = [
|
|
138
|
+
{ value: 'global', label: `Global (~/) ${pc.dim(`(${globalSkills.length})`)}` },
|
|
139
|
+
{ value: 'project', label: `Project (./) ${pc.dim(`(${projectSkills.length})`)}` },
|
|
140
|
+
];
|
|
141
|
+
const selectedLocation = await p.select({
|
|
142
|
+
message: 'Select location',
|
|
143
|
+
options: locationOptions,
|
|
144
|
+
});
|
|
145
|
+
if (p.isCancel(selectedLocation)) {
|
|
146
|
+
p.cancel('Cancelled');
|
|
147
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
148
|
+
}
|
|
149
|
+
const skillsToShow = selectedLocation === 'global' ? globalSkills : projectSkills;
|
|
150
|
+
const locationLabel = selectedLocation === 'global' ? 'Global (~/)' : 'Project (./)';
|
|
151
|
+
displaySkillsForLocation(skillsToShow, locationLabel);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
// Non-interactive or single location: show available skills
|
|
155
|
+
if (globalSkills.length > 0) {
|
|
156
|
+
displaySkillsForLocation(globalSkills, 'Global (~/)');
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
displaySkillsForLocation(projectSkills, 'Project (./)');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
163
|
+
}
|
|
164
|
+
// JSON mode without agent filter: return all skills
|
|
165
|
+
if (jsonMode) {
|
|
166
|
+
const { installed } = collectSkills(detected);
|
|
167
|
+
outputJsonAndExit(EXIT_CODES.SUCCESS, {
|
|
168
|
+
installed,
|
|
169
|
+
agents_detected: detected.map((a) => a.name),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
// Interactive mode: show agent selector
|
|
173
|
+
if (isInputTTY()) {
|
|
174
|
+
console.log();
|
|
175
|
+
p.intro(`${pc.bgCyan(pc.black(' skillfish '))} ${pc.dim('Installed skills')}`);
|
|
176
|
+
// Step 1: Build options with skill counts in label (always visible)
|
|
177
|
+
const agentOptions = detected.map((agent) => {
|
|
178
|
+
const { installed } = collectSkills([agent]);
|
|
179
|
+
const count = installed.length;
|
|
180
|
+
return {
|
|
181
|
+
value: agent.name,
|
|
182
|
+
label: `${agent.name} ${pc.dim(`(${count})`)}`,
|
|
183
|
+
};
|
|
184
|
+
});
|
|
185
|
+
const selected = await p.select({
|
|
186
|
+
message: 'Select an agent',
|
|
187
|
+
options: agentOptions,
|
|
188
|
+
});
|
|
189
|
+
if (p.isCancel(selected)) {
|
|
190
|
+
p.cancel('Cancelled');
|
|
191
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
192
|
+
}
|
|
193
|
+
const selectedAgent = detected.find((a) => a.name === selected);
|
|
194
|
+
if (!selectedAgent) {
|
|
195
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
196
|
+
}
|
|
197
|
+
// Step 2: Get skills for selected agent
|
|
198
|
+
const { globalSkills, projectSkills } = collectSkills([selectedAgent]);
|
|
199
|
+
const hasBothLocations = globalSkills.length > 0 && projectSkills.length > 0;
|
|
200
|
+
if (globalSkills.length === 0 && projectSkills.length === 0) {
|
|
201
|
+
p.log.info(`No skills installed for ${pc.cyan(selectedAgent.name)}`);
|
|
202
|
+
p.outro(pc.dim('Done'));
|
|
203
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
204
|
+
}
|
|
205
|
+
// Step 3: If both locations have skills, let user choose location
|
|
206
|
+
let skillsToShow;
|
|
207
|
+
let locationLabel;
|
|
208
|
+
if (hasBothLocations) {
|
|
209
|
+
const locationOptions = [
|
|
210
|
+
{ value: 'global', label: `Global (~/) ${pc.dim(`(${globalSkills.length})`)}` },
|
|
211
|
+
{ value: 'project', label: `Project (./) ${pc.dim(`(${projectSkills.length})`)}` },
|
|
212
|
+
];
|
|
213
|
+
const selectedLocation = await p.select({
|
|
214
|
+
message: 'Select location',
|
|
215
|
+
options: locationOptions,
|
|
216
|
+
});
|
|
217
|
+
if (p.isCancel(selectedLocation)) {
|
|
218
|
+
p.cancel('Cancelled');
|
|
219
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
220
|
+
}
|
|
221
|
+
skillsToShow = selectedLocation === 'global' ? globalSkills : projectSkills;
|
|
222
|
+
locationLabel = selectedLocation === 'global' ? 'Global (~/)' : 'Project (./)';
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
// Only one location has skills, use that
|
|
226
|
+
if (globalSkills.length > 0) {
|
|
227
|
+
skillsToShow = globalSkills;
|
|
228
|
+
locationLabel = 'Global (~/)';
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
skillsToShow = projectSkills;
|
|
232
|
+
locationLabel = 'Project (./)';
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// Step 4: Display skills for selected location
|
|
236
|
+
displaySkillsForLocation(skillsToShow, locationLabel);
|
|
237
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
238
|
+
}
|
|
239
|
+
// Non-interactive mode: display all agents with skills
|
|
240
|
+
const { installed, globalSkills, projectSkills } = collectSkills(detected);
|
|
241
|
+
console.log();
|
|
242
|
+
p.intro(`${pc.bgCyan(pc.black(' skillfish '))} ${pc.dim('Installed skills')}`);
|
|
243
|
+
console.log();
|
|
244
|
+
console.log(pc.bold('Detected Agents'));
|
|
245
|
+
console.log(` ${detected.map((a) => a.name).join(', ')}`);
|
|
246
|
+
// Group by agent
|
|
247
|
+
function displayByAgent(skills, location) {
|
|
248
|
+
const byAgent = new Map();
|
|
249
|
+
for (const item of skills) {
|
|
250
|
+
const list = byAgent.get(item.agent) || [];
|
|
251
|
+
list.push(item.skill);
|
|
252
|
+
byAgent.set(item.agent, list);
|
|
253
|
+
}
|
|
254
|
+
if (byAgent.size === 0)
|
|
255
|
+
return false;
|
|
256
|
+
console.log();
|
|
257
|
+
console.log(pc.bold(pc.underline(location)));
|
|
258
|
+
for (const [agent, agentSkills] of byAgent) {
|
|
259
|
+
console.log(` ${pc.cyan(agent)} ${pc.dim(`(${agentSkills.length})`)}`);
|
|
260
|
+
for (const skill of agentSkills) {
|
|
261
|
+
console.log(` ${pc.green('•')} ${skill}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
if (checkGlobal)
|
|
267
|
+
displayByAgent(globalSkills, 'Global (~/)');
|
|
268
|
+
if (checkProject)
|
|
269
|
+
displayByAgent(projectSkills, 'Project (./)');
|
|
270
|
+
console.log();
|
|
271
|
+
if (installed.length === 0) {
|
|
272
|
+
p.outro(pc.dim('No skills installed'));
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
p.outro(`${pc.cyan(installed.length.toString())} skill${installed.length === 1 ? '' : 's'} total`);
|
|
276
|
+
}
|
|
277
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
278
|
+
});
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `skillfish remove` command - Remove installed skills.
|
|
3
|
+
*/
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
import { homedir } from 'os';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { existsSync, rmSync } from 'fs';
|
|
8
|
+
import * as p from '@clack/prompts';
|
|
9
|
+
import pc from 'picocolors';
|
|
10
|
+
import { getDetectedAgents, getAgentSkillDir } from '../lib/agents.js';
|
|
11
|
+
import { listInstalledSkillsInDir } from '../lib/installer.js';
|
|
12
|
+
import { isTTY, isInputTTY } from '../utils.js';
|
|
13
|
+
import { EXIT_CODES } from '../lib/constants.js';
|
|
14
|
+
// === Command Definition ===
|
|
15
|
+
export const removeCommand = new Command('remove')
|
|
16
|
+
.description('Remove an installed skill from your agents')
|
|
17
|
+
.argument('[skill]', 'Name of the skill to remove')
|
|
18
|
+
.option('-y, --yes', 'Skip confirmation prompts')
|
|
19
|
+
.option('--all', 'Remove all installed skills')
|
|
20
|
+
.option('--project', 'Remove from current project only (./.claude)')
|
|
21
|
+
.option('--global', 'Remove from home directory only (~/.claude)')
|
|
22
|
+
.option('--agent <name>', 'Remove from a specific agent only')
|
|
23
|
+
.helpOption('-h, --help', 'Display help for command')
|
|
24
|
+
.addHelpText('after', `
|
|
25
|
+
Examples:
|
|
26
|
+
$ skillfish remove Interactive skill picker
|
|
27
|
+
$ skillfish remove my-skill Remove a skill by name
|
|
28
|
+
$ skillfish remove --all Remove all installed skills
|
|
29
|
+
$ skillfish remove my-skill --project Remove from current project only
|
|
30
|
+
$ skillfish remove my-skill --agent "Claude Code" Remove from specific agent`)
|
|
31
|
+
.action(async (skillArg, options, command) => {
|
|
32
|
+
const jsonMode = command.parent?.opts().json ?? false;
|
|
33
|
+
const version = command.parent?.opts().version ?? '0.0.0';
|
|
34
|
+
const result = {
|
|
35
|
+
success: true,
|
|
36
|
+
removed: [],
|
|
37
|
+
errors: [],
|
|
38
|
+
};
|
|
39
|
+
function addError(message) {
|
|
40
|
+
result.errors.push(message);
|
|
41
|
+
result.success = false;
|
|
42
|
+
}
|
|
43
|
+
function outputJsonAndExit(exitCode) {
|
|
44
|
+
result.exit_code = exitCode;
|
|
45
|
+
console.log(JSON.stringify(result, null, 2));
|
|
46
|
+
process.exit(exitCode);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Unified error handler that handles both JSON and TTY modes.
|
|
50
|
+
*/
|
|
51
|
+
function exitWithError(message, exitCode, useClackLog = false) {
|
|
52
|
+
if (jsonMode) {
|
|
53
|
+
addError(message);
|
|
54
|
+
outputJsonAndExit(exitCode);
|
|
55
|
+
}
|
|
56
|
+
if (useClackLog) {
|
|
57
|
+
p.log.error(message);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
console.error(`Error: ${message}`);
|
|
61
|
+
}
|
|
62
|
+
process.exit(exitCode);
|
|
63
|
+
}
|
|
64
|
+
// Show banner (TTY only, not in JSON mode)
|
|
65
|
+
if (isTTY() && !jsonMode) {
|
|
66
|
+
console.log();
|
|
67
|
+
console.log(pc.cyan(' ≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋'));
|
|
68
|
+
console.log(` ${pc.cyan('><>')} ${pc.bold('SKILL FISH')} ${pc.cyan('><>')}`);
|
|
69
|
+
console.log(pc.cyan(' ≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋'));
|
|
70
|
+
console.log();
|
|
71
|
+
p.intro(`${pc.bgCyan(pc.black(' skillfish '))} ${pc.dim(`v${version}`)}`);
|
|
72
|
+
}
|
|
73
|
+
const skipConfirm = options.yes ?? false;
|
|
74
|
+
const removeAll = options.all ?? false;
|
|
75
|
+
const projectFlag = options.project ?? false;
|
|
76
|
+
const globalFlag = options.global ?? false;
|
|
77
|
+
const targetAgentName = options.agent;
|
|
78
|
+
// Determine which locations to check
|
|
79
|
+
// By default, check both global and project. Flags narrow it down.
|
|
80
|
+
const checkGlobal = !projectFlag; // Check global unless --project is set
|
|
81
|
+
const checkProject = !globalFlag; // Check project unless --global is set
|
|
82
|
+
// Detect agents
|
|
83
|
+
const detected = getDetectedAgents();
|
|
84
|
+
if (detected.length === 0) {
|
|
85
|
+
exitWithError('No agents detected. Install Claude Code, Cursor, or another supported agent first.', EXIT_CODES.GENERAL_ERROR, true // useClackLog
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
// Filter to target agent if specified
|
|
89
|
+
let targetAgents = detected;
|
|
90
|
+
if (targetAgentName) {
|
|
91
|
+
const found = detected.filter((a) => a.name.toLowerCase() === targetAgentName.toLowerCase());
|
|
92
|
+
if (found.length === 0) {
|
|
93
|
+
exitWithError(`Agent "${targetAgentName}" not found. Detected agents: ${detected.map((a) => a.name).join(', ')}`, EXIT_CODES.NOT_FOUND, true // useClackLog
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
targetAgents = found;
|
|
97
|
+
}
|
|
98
|
+
// Helper to collect all installed skills across locations
|
|
99
|
+
function collectAllSkills() {
|
|
100
|
+
const skills = [];
|
|
101
|
+
const seenPaths = new Set();
|
|
102
|
+
for (const agent of targetAgents) {
|
|
103
|
+
if (checkGlobal) {
|
|
104
|
+
const globalDir = getAgentSkillDir(agent, homedir());
|
|
105
|
+
const installed = listInstalledSkillsInDir(globalDir);
|
|
106
|
+
for (const skill of installed) {
|
|
107
|
+
const skillPath = join(globalDir, skill);
|
|
108
|
+
if (!seenPaths.has(skillPath)) {
|
|
109
|
+
seenPaths.add(skillPath);
|
|
110
|
+
skills.push({ skill, agent, path: skillPath, location: 'global' });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (checkProject) {
|
|
115
|
+
const projectDir = getAgentSkillDir(agent, process.cwd());
|
|
116
|
+
const installed = listInstalledSkillsInDir(projectDir);
|
|
117
|
+
for (const skill of installed) {
|
|
118
|
+
const skillPath = join(projectDir, skill);
|
|
119
|
+
if (!seenPaths.has(skillPath)) {
|
|
120
|
+
seenPaths.add(skillPath);
|
|
121
|
+
skills.push({ skill, agent, path: skillPath, location: 'project' });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return skills;
|
|
127
|
+
}
|
|
128
|
+
// Helper to perform the actual removal
|
|
129
|
+
async function performRemoval(skillsToRemove) {
|
|
130
|
+
for (const item of skillsToRemove) {
|
|
131
|
+
try {
|
|
132
|
+
if (existsSync(item.path)) {
|
|
133
|
+
rmSync(item.path, { recursive: true });
|
|
134
|
+
result.removed.push({
|
|
135
|
+
skill: item.skill,
|
|
136
|
+
agent: item.agent.name,
|
|
137
|
+
path: item.path,
|
|
138
|
+
});
|
|
139
|
+
if (!jsonMode) {
|
|
140
|
+
console.log(` ${pc.green('✓')} Removed ${item.skill} ${pc.dim(`from ${item.agent.name}`)}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
const errorMsg = `Failed to remove ${item.skill}: ${err instanceof Error ? err.message : String(err)}`;
|
|
146
|
+
addError(errorMsg);
|
|
147
|
+
if (!jsonMode) {
|
|
148
|
+
console.log(` ${pc.red('✗')} ${errorMsg}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Helper to collect skills for a specific agent
|
|
154
|
+
function collectSkillsForAgent(agent) {
|
|
155
|
+
const skills = [];
|
|
156
|
+
const seenPaths = new Set();
|
|
157
|
+
if (checkGlobal) {
|
|
158
|
+
const globalDir = getAgentSkillDir(agent, homedir());
|
|
159
|
+
const installed = listInstalledSkillsInDir(globalDir);
|
|
160
|
+
for (const skill of installed) {
|
|
161
|
+
const skillPath = join(globalDir, skill);
|
|
162
|
+
if (!seenPaths.has(skillPath)) {
|
|
163
|
+
seenPaths.add(skillPath);
|
|
164
|
+
skills.push({ skill, agent, path: skillPath, location: 'global' });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (checkProject) {
|
|
169
|
+
const projectDir = getAgentSkillDir(agent, process.cwd());
|
|
170
|
+
const installed = listInstalledSkillsInDir(projectDir);
|
|
171
|
+
for (const skill of installed) {
|
|
172
|
+
const skillPath = join(projectDir, skill);
|
|
173
|
+
if (!seenPaths.has(skillPath)) {
|
|
174
|
+
seenPaths.add(skillPath);
|
|
175
|
+
skills.push({ skill, agent, path: skillPath, location: 'project' });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return skills;
|
|
180
|
+
}
|
|
181
|
+
// Interactive mode: no skill name and no --all flag
|
|
182
|
+
if (!skillArg && !removeAll) {
|
|
183
|
+
// In non-interactive mode, require explicit skill name or --all
|
|
184
|
+
if (!isInputTTY() || jsonMode) {
|
|
185
|
+
exitWithError('Please specify a skill name or use --all to remove all skills (non-interactive mode)', EXIT_CODES.INVALID_ARGS);
|
|
186
|
+
}
|
|
187
|
+
// Step 1: Show agent selector with skill counts
|
|
188
|
+
const agentOptions = targetAgents.map((agent) => {
|
|
189
|
+
const skills = collectSkillsForAgent(agent);
|
|
190
|
+
const count = skills.length;
|
|
191
|
+
return {
|
|
192
|
+
value: agent.name,
|
|
193
|
+
label: `${agent.name} ${pc.dim(`(${count})`)}`,
|
|
194
|
+
};
|
|
195
|
+
});
|
|
196
|
+
const selectedAgentName = await p.select({
|
|
197
|
+
message: 'Select an agent',
|
|
198
|
+
options: agentOptions,
|
|
199
|
+
});
|
|
200
|
+
if (p.isCancel(selectedAgentName)) {
|
|
201
|
+
p.cancel('Cancelled');
|
|
202
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
203
|
+
}
|
|
204
|
+
const selectedAgent = targetAgents.find((a) => a.name === selectedAgentName);
|
|
205
|
+
if (!selectedAgent) {
|
|
206
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
207
|
+
}
|
|
208
|
+
// Step 2: Get skills for selected agent, split by location
|
|
209
|
+
const agentSkills = collectSkillsForAgent(selectedAgent);
|
|
210
|
+
const globalSkills = agentSkills.filter((s) => s.location === 'global');
|
|
211
|
+
const projectSkills = agentSkills.filter((s) => s.location === 'project');
|
|
212
|
+
if (agentSkills.length === 0) {
|
|
213
|
+
p.log.info(`No skills installed for ${pc.cyan(selectedAgent.name)}`);
|
|
214
|
+
p.outro(pc.dim('Done'));
|
|
215
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
216
|
+
}
|
|
217
|
+
// Step 3: If both locations have skills, let user choose location
|
|
218
|
+
let skillsToShow;
|
|
219
|
+
const hasBothLocations = globalSkills.length > 0 && projectSkills.length > 0;
|
|
220
|
+
if (hasBothLocations) {
|
|
221
|
+
const locationOptions = [
|
|
222
|
+
{ value: 'global', label: `Global (~/) ${pc.dim(`(${globalSkills.length})`)}` },
|
|
223
|
+
{ value: 'project', label: `Project (./) ${pc.dim(`(${projectSkills.length})`)}` },
|
|
224
|
+
];
|
|
225
|
+
const selectedLocation = await p.select({
|
|
226
|
+
message: 'Select location',
|
|
227
|
+
options: locationOptions,
|
|
228
|
+
});
|
|
229
|
+
if (p.isCancel(selectedLocation)) {
|
|
230
|
+
p.cancel('Cancelled');
|
|
231
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
232
|
+
}
|
|
233
|
+
skillsToShow = selectedLocation === 'global' ? globalSkills : projectSkills;
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
// Only one location has skills, use that
|
|
237
|
+
skillsToShow = globalSkills.length > 0 ? globalSkills : projectSkills;
|
|
238
|
+
}
|
|
239
|
+
// Step 4: Show skill selector for the chosen location
|
|
240
|
+
const skillOptions = skillsToShow.map((item) => ({
|
|
241
|
+
value: item.path,
|
|
242
|
+
label: item.skill,
|
|
243
|
+
}));
|
|
244
|
+
const toRemove = await p.multiselect({
|
|
245
|
+
message: `Select skills to remove ${pc.dim('(space to select, enter to confirm)')}`,
|
|
246
|
+
options: skillOptions,
|
|
247
|
+
required: false,
|
|
248
|
+
});
|
|
249
|
+
if (p.isCancel(toRemove)) {
|
|
250
|
+
p.cancel('Cancelled');
|
|
251
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
252
|
+
}
|
|
253
|
+
if (toRemove.length === 0) {
|
|
254
|
+
p.outro(pc.dim('No skills selected'));
|
|
255
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
256
|
+
}
|
|
257
|
+
const selectedSkills = skillsToShow.filter((s) => toRemove.includes(s.path));
|
|
258
|
+
// Confirm removal
|
|
259
|
+
console.log();
|
|
260
|
+
p.log.warn(pc.yellow('Skills to remove:'));
|
|
261
|
+
for (const item of selectedSkills) {
|
|
262
|
+
console.log(` ${pc.red('•')} ${item.skill}`);
|
|
263
|
+
}
|
|
264
|
+
const confirm = await p.confirm({
|
|
265
|
+
message: `Remove ${selectedSkills.length} skill${selectedSkills.length === 1 ? '' : 's'}?`,
|
|
266
|
+
initialValue: false,
|
|
267
|
+
});
|
|
268
|
+
if (p.isCancel(confirm) || !confirm) {
|
|
269
|
+
p.cancel('Cancelled');
|
|
270
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
271
|
+
}
|
|
272
|
+
// Perform removal
|
|
273
|
+
await performRemoval(selectedSkills);
|
|
274
|
+
// Output results
|
|
275
|
+
console.log();
|
|
276
|
+
if (result.removed.length > 0) {
|
|
277
|
+
p.outro(pc.green(`Done! Removed ${result.removed.length} skill${result.removed.length === 1 ? '' : 's'}`));
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
p.outro(pc.yellow('No skills removed'));
|
|
281
|
+
}
|
|
282
|
+
process.exit(result.success ? EXIT_CODES.SUCCESS : EXIT_CODES.GENERAL_ERROR);
|
|
283
|
+
}
|
|
284
|
+
// Non-interactive mode: find skills to remove based on skillArg or --all
|
|
285
|
+
const allSkills = collectAllSkills();
|
|
286
|
+
let skillsToRemove;
|
|
287
|
+
if (removeAll) {
|
|
288
|
+
skillsToRemove = allSkills;
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
// Filter to matching skill name
|
|
292
|
+
skillsToRemove = allSkills.filter((s) => s.skill === skillArg);
|
|
293
|
+
}
|
|
294
|
+
if (skillsToRemove.length === 0) {
|
|
295
|
+
const errorMsg = removeAll
|
|
296
|
+
? 'No skills installed to remove'
|
|
297
|
+
: `Skill "${skillArg}" not found`;
|
|
298
|
+
if (jsonMode) {
|
|
299
|
+
addError(errorMsg);
|
|
300
|
+
outputJsonAndExit(EXIT_CODES.NOT_FOUND);
|
|
301
|
+
}
|
|
302
|
+
p.log.warn(errorMsg);
|
|
303
|
+
process.exit(EXIT_CODES.NOT_FOUND);
|
|
304
|
+
}
|
|
305
|
+
// Confirmation prompt (unless --yes is used)
|
|
306
|
+
if (!skipConfirm && !jsonMode && isInputTTY()) {
|
|
307
|
+
console.log();
|
|
308
|
+
p.log.warn(pc.yellow('The following skills will be removed:'));
|
|
309
|
+
for (const item of skillsToRemove) {
|
|
310
|
+
console.log(` ${pc.red('•')} ${item.skill} ${pc.dim(`(${item.agent.name}, ${item.location})`)}`);
|
|
311
|
+
}
|
|
312
|
+
console.log();
|
|
313
|
+
const proceed = await p.confirm({
|
|
314
|
+
message: `Remove ${skillsToRemove.length} skill${skillsToRemove.length === 1 ? '' : 's'}?`,
|
|
315
|
+
initialValue: false,
|
|
316
|
+
});
|
|
317
|
+
if (p.isCancel(proceed) || !proceed) {
|
|
318
|
+
p.cancel('Cancelled');
|
|
319
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// Perform removal
|
|
323
|
+
await performRemoval(skillsToRemove);
|
|
324
|
+
// Output results
|
|
325
|
+
if (jsonMode) {
|
|
326
|
+
outputJsonAndExit(result.success ? EXIT_CODES.SUCCESS : EXIT_CODES.GENERAL_ERROR);
|
|
327
|
+
}
|
|
328
|
+
console.log();
|
|
329
|
+
if (result.removed.length > 0) {
|
|
330
|
+
p.outro(pc.green(`Done! Removed ${result.removed.length} skill${result.removed.length === 1 ? '' : 's'}`));
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
p.outro(pc.yellow('No skills removed'));
|
|
334
|
+
}
|
|
335
|
+
process.exit(result.success ? EXIT_CODES.SUCCESS : EXIT_CODES.GENERAL_ERROR);
|
|
336
|
+
});
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* skillfish CLI - Install AI agent skills from GitHub
|
|
4
|
+
*
|
|
5
|
+
* Entry point that sets up Commander.js and imports commands.
|
|
6
|
+
*/
|
|
7
|
+
import { Command } from 'commander';
|
|
8
|
+
import { readFileSync } from 'fs';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { dirname, join } from 'path';
|
|
11
|
+
import { addCommand } from './commands/add.js';
|
|
12
|
+
import { listCommand } from './commands/list.js';
|
|
13
|
+
import { removeCommand } from './commands/remove.js';
|
|
14
|
+
// Read version from package.json (single source of truth)
|
|
15
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
17
|
+
const program = new Command()
|
|
18
|
+
.name('skillfish')
|
|
19
|
+
.description('Install and manage AI agent skills from GitHub repositories')
|
|
20
|
+
.version(pkg.version, '-v, --version', 'Show version number')
|
|
21
|
+
.option('--json', 'Output as JSON (for automation)')
|
|
22
|
+
.helpOption('-h, --help', 'Display help for command')
|
|
23
|
+
.helpCommand('help [command]', 'Display help for command')
|
|
24
|
+
.configureOutput({
|
|
25
|
+
// Write help to stdout so it can be piped
|
|
26
|
+
writeOut: (str) => process.stdout.write(str),
|
|
27
|
+
writeErr: (str) => process.stderr.write(str),
|
|
28
|
+
})
|
|
29
|
+
.configureHelp({
|
|
30
|
+
sortSubcommands: true,
|
|
31
|
+
})
|
|
32
|
+
.addHelpText('after', `
|
|
33
|
+
Examples:
|
|
34
|
+
$ skillfish add owner/repo Install skills from a repository
|
|
35
|
+
$ skillfish add owner/repo/plugin/skill Install a specific skill
|
|
36
|
+
$ skillfish list Show installed skills
|
|
37
|
+
$ skillfish remove my-skill Remove a skill
|
|
38
|
+
|
|
39
|
+
Documentation: https://skill.fish`);
|
|
40
|
+
// Store version in options for commands to access
|
|
41
|
+
program.hook('preAction', (thisCommand) => {
|
|
42
|
+
thisCommand.setOptionValue('version', pkg.version);
|
|
43
|
+
});
|
|
44
|
+
// Add subcommands
|
|
45
|
+
program.addCommand(addCommand);
|
|
46
|
+
program.addCommand(listCommand);
|
|
47
|
+
program.addCommand(removeCommand);
|
|
48
|
+
// Handle --json flag for help output
|
|
49
|
+
program.on('option:json', () => {
|
|
50
|
+
// JSON mode is handled by commands
|
|
51
|
+
});
|
|
52
|
+
// Parse and run
|
|
53
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
54
|
+
console.error('Error:', err instanceof Error ? err.message : String(err));
|
|
55
|
+
process.exit(1);
|
|
56
|
+
});
|