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.
@@ -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,5 @@
1
+ /**
2
+ * `skillfish remove` command - Remove installed skills.
3
+ */
4
+ import { Command } from 'commander';
5
+ export declare const removeCommand: Command;
@@ -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
+ });
@@ -0,0 +1,7 @@
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
+ export {};
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
+ });