skillfish 1.0.15 → 1.0.17
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 +21 -1
- package/dist/commands/add.js +16 -10
- package/dist/commands/init.d.ts +5 -0
- package/dist/commands/init.js +499 -0
- package/dist/commands/update.js +14 -9
- package/dist/index.js +3 -0
- package/dist/lib/github.d.ts +26 -1
- package/dist/lib/github.js +53 -11
- package/dist/utils.d.ts +15 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -47,6 +47,7 @@ Learn more at [agentskills.io](https://agentskills.io).
|
|
|
47
47
|
| Command | Description |
|
|
48
48
|
|---------|-------------|
|
|
49
49
|
| `skillfish add <owner/repo>` | Install skills |
|
|
50
|
+
| `skillfish init` | Create a new skill |
|
|
50
51
|
| `skillfish list` | View installed skills |
|
|
51
52
|
| `skillfish remove [name]` | Remove skills |
|
|
52
53
|
| `skillfish update` | Update installed skills |
|
|
@@ -56,8 +57,10 @@ All commands support `--json` for automation.
|
|
|
56
57
|
## Examples
|
|
57
58
|
|
|
58
59
|
```bash
|
|
59
|
-
skillfish add
|
|
60
|
+
skillfish add owner/repo # Install from a repository
|
|
60
61
|
skillfish add owner/repo --all # Install all skills from repo
|
|
62
|
+
skillfish init # Create a new skill (interactive)
|
|
63
|
+
skillfish init --name my-skill # Create with a specified name
|
|
61
64
|
skillfish list # See what's installed
|
|
62
65
|
skillfish update # Update all skills
|
|
63
66
|
skillfish remove old-skill # Remove a skill
|
|
@@ -114,6 +117,23 @@ skillfish add owner/repo --project # Project only (./)
|
|
|
114
117
|
skillfish add owner/repo --global # Global only (~/)
|
|
115
118
|
```
|
|
116
119
|
|
|
120
|
+
### init
|
|
121
|
+
|
|
122
|
+
Create a new skill template with `SKILL.md` and optional directories.
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
skillfish init # Interactive skill creation
|
|
126
|
+
skillfish init --name my-skill # Specify skill name
|
|
127
|
+
skillfish init --name my-skill --description "Does a thing" # Non-interactive
|
|
128
|
+
skillfish init --project # Create in current project (./)
|
|
129
|
+
skillfish init --global # Create in home directory (~/)
|
|
130
|
+
skillfish init --name my-skill --yes # Skip all prompts
|
|
131
|
+
skillfish init --author "your-name" # Set author metadata
|
|
132
|
+
skillfish init --license MIT # Set license
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Interactive mode prompts for name, description, optional metadata (author, license), optional directories (`scripts/`, `references/`, `assets/`), install location, and target agents.
|
|
136
|
+
|
|
117
137
|
### list
|
|
118
138
|
|
|
119
139
|
View installed skills.
|
package/dist/commands/add.js
CHANGED
|
@@ -9,7 +9,7 @@ import pc from 'picocolors';
|
|
|
9
9
|
import { trackInstall } from '../telemetry.js';
|
|
10
10
|
import { isValidPath, parseFrontmatter, deriveSkillName, toTitleCase, truncate, batchMap, createJsonOutput, isInputTTY, isTTY, } from '../utils.js';
|
|
11
11
|
import { getDetectedAgents, AGENT_CONFIGS } from '../lib/agents.js';
|
|
12
|
-
import { findAllSkillMdFiles, fetchSkillMdContent, fetchDefaultBranch, fetchTreeSha, SKILL_FILENAME, RateLimitError, RepoNotFoundError, NetworkError, GitHubApiError, } from '../lib/github.js';
|
|
12
|
+
import { findAllSkillMdFiles, fetchSkillMdContent, fetchDefaultBranch, fetchTreeSha, getSkillSha, SKILL_FILENAME, RateLimitError, RepoNotFoundError, NetworkError, GitHubApiError, } from '../lib/github.js';
|
|
13
13
|
import { installSkill } from '../lib/installer.js';
|
|
14
14
|
import { EXIT_CODES, isValidName } from '../lib/constants.js';
|
|
15
15
|
// === Command Definition ===
|
|
@@ -127,11 +127,13 @@ Examples:
|
|
|
127
127
|
// If we can't fetch SHA, install without manifest tracking
|
|
128
128
|
sha = undefined;
|
|
129
129
|
}
|
|
130
|
-
|
|
130
|
+
// For explicit paths, we don't have the tree (would require extra API call)
|
|
131
|
+
// The sha will be root tree SHA - acceptable for explicit path installs
|
|
132
|
+
discoveryResult = { paths: [explicitPath], branch, sha, tree: [] };
|
|
131
133
|
}
|
|
132
134
|
catch {
|
|
133
135
|
// If we can't fetch the branch, let degit try its own detection
|
|
134
|
-
discoveryResult = { paths: [explicitPath], branch: undefined, sha: undefined };
|
|
136
|
+
discoveryResult = { paths: [explicitPath], branch: undefined, sha: undefined, tree: [] };
|
|
135
137
|
}
|
|
136
138
|
}
|
|
137
139
|
else {
|
|
@@ -143,7 +145,7 @@ Examples:
|
|
|
143
145
|
}
|
|
144
146
|
process.exit(EXIT_CODES.NOT_FOUND);
|
|
145
147
|
}
|
|
146
|
-
const { paths: skillPaths, branch: discoveredBranch, sha: discoveredSha } = discoveryResult;
|
|
148
|
+
const { paths: skillPaths, branch: discoveredBranch, sha: discoveredSha, tree: discoveredTree, } = discoveryResult;
|
|
147
149
|
// 2. Determine install location (global vs project)
|
|
148
150
|
const baseDir = await selectInstallLocation(projectFlag, globalFlag, jsonMode);
|
|
149
151
|
// 3. Select agents to install to
|
|
@@ -198,11 +200,15 @@ Examples:
|
|
|
198
200
|
spinner = p.spinner();
|
|
199
201
|
spinner.start(`Downloading ${skillName}...`);
|
|
200
202
|
}
|
|
203
|
+
// Get directory-specific SHA for better update tracking
|
|
204
|
+
// skillPath is either 'SKILL.md' or a directory like 'skills/foo'
|
|
205
|
+
const skillMdPath = skillPath === SKILL_FILENAME ? SKILL_FILENAME : `${skillPath}/${SKILL_FILENAME}`;
|
|
206
|
+
const skillSha = getSkillSha(discoveredTree, skillMdPath) ?? discoveredSha;
|
|
201
207
|
const result = await installSkill(owner, repo, skillPath, skillName, targetAgents, {
|
|
202
208
|
force,
|
|
203
209
|
baseDir,
|
|
204
210
|
branch: discoveredBranch,
|
|
205
|
-
sha:
|
|
211
|
+
sha: skillSha,
|
|
206
212
|
});
|
|
207
213
|
if (spinner) {
|
|
208
214
|
if (result.failed) {
|
|
@@ -383,7 +389,7 @@ async function discoverSkillPaths(owner, repo, installAll, jsonMode, jsonOutput,
|
|
|
383
389
|
}
|
|
384
390
|
process.exit(exitCode);
|
|
385
391
|
}
|
|
386
|
-
const { paths: skillPaths, branch, sha } = skillDiscovery;
|
|
392
|
+
const { paths: skillPaths, branch, sha, tree } = skillDiscovery;
|
|
387
393
|
if (skillPaths.length === 0) {
|
|
388
394
|
const errorMsg = `No ${SKILL_FILENAME} found in repository`;
|
|
389
395
|
if (jsonMode) {
|
|
@@ -442,7 +448,7 @@ async function discoverSkillPaths(owner, repo, installAll, jsonMode, jsonOutput,
|
|
|
442
448
|
if (!jsonMode) {
|
|
443
449
|
p.log.info(`${pc.bold(displayName)}${desc ? pc.dim(` - ${desc}`) : ''}`);
|
|
444
450
|
}
|
|
445
|
-
return { paths: [matchedSkill.dir], branch, sha };
|
|
451
|
+
return { paths: [matchedSkill.dir], branch, sha, tree };
|
|
446
452
|
}
|
|
447
453
|
// Skill not found - show available skills
|
|
448
454
|
const errorMsg = `Skill "${targetSkillName}" not found in repository`;
|
|
@@ -468,7 +474,7 @@ async function discoverSkillPaths(owner, repo, installAll, jsonMode, jsonOutput,
|
|
|
468
474
|
if (!jsonMode) {
|
|
469
475
|
p.log.info(`${pc.bold(displayName)}${desc ? pc.dim(` - ${desc}`) : ''}`);
|
|
470
476
|
}
|
|
471
|
-
return { paths: [skill.dir], branch, sha };
|
|
477
|
+
return { paths: [skill.dir], branch, sha, tree };
|
|
472
478
|
}
|
|
473
479
|
// Build options for selection with frontmatter metadata
|
|
474
480
|
// No hints - @clack/prompts has rendering issues with long option lists
|
|
@@ -483,7 +489,7 @@ async function discoverSkillPaths(owner, repo, installAll, jsonMode, jsonOutput,
|
|
|
483
489
|
if (!jsonMode) {
|
|
484
490
|
console.log(`Installing all ${skills.length} skills`);
|
|
485
491
|
}
|
|
486
|
-
return { paths: skills.map((s) => s.dir), branch, sha };
|
|
492
|
+
return { paths: skills.map((s) => s.dir), branch, sha, tree };
|
|
487
493
|
}
|
|
488
494
|
// Otherwise, list skills and exit with guidance
|
|
489
495
|
if (jsonMode) {
|
|
@@ -511,7 +517,7 @@ async function discoverSkillPaths(owner, repo, installAll, jsonMode, jsonOutput,
|
|
|
511
517
|
p.cancel('Cancelled');
|
|
512
518
|
process.exit(EXIT_CODES.SUCCESS);
|
|
513
519
|
}
|
|
514
|
-
return { paths: selected, branch, sha };
|
|
520
|
+
return { paths: selected, branch, sha, tree };
|
|
515
521
|
}
|
|
516
522
|
/**
|
|
517
523
|
* SECURITY: Show warning and ask for user confirmation before installing.
|
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `skillfish init` command - Generate a template skill.
|
|
3
|
+
*/
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
import { homedir } from 'os';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
8
|
+
import * as p from '@clack/prompts';
|
|
9
|
+
import pc from 'picocolors';
|
|
10
|
+
import { getDetectedAgents, getAgentSkillDir } from '../lib/agents.js';
|
|
11
|
+
import { SKILL_FILENAME } from '../lib/github.js';
|
|
12
|
+
import { EXIT_CODES } from '../lib/constants.js';
|
|
13
|
+
import { isInputTTY, isTTY } from '../utils.js';
|
|
14
|
+
// === Validation ===
|
|
15
|
+
/**
|
|
16
|
+
* Validates skill name format.
|
|
17
|
+
* Must be lowercase, use hyphens or underscores, no spaces, no consecutive hyphens.
|
|
18
|
+
*/
|
|
19
|
+
function isValidSkillName(name) {
|
|
20
|
+
// Must start and end with alphanumeric, contain only lowercase, numbers, hyphens, underscores
|
|
21
|
+
if (!/^[a-z0-9][a-z0-9_-]*[a-z0-9]$|^[a-z0-9]$/.test(name)) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
// No consecutive hyphens or underscores
|
|
25
|
+
if (/[-_]{2,}/.test(name)) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
// Reasonable length
|
|
29
|
+
if (name.length < 1 || name.length > 64) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Convert user input to valid skill name format.
|
|
36
|
+
*/
|
|
37
|
+
function normalizeSkillName(input) {
|
|
38
|
+
return input
|
|
39
|
+
.toLowerCase()
|
|
40
|
+
.trim()
|
|
41
|
+
.replace(/\s+/g, '-') // spaces to hyphens
|
|
42
|
+
.replace(/[^a-z0-9_-]/g, '') // remove invalid chars
|
|
43
|
+
.replace(/[-_]{2,}/g, '-'); // collapse multiple separators
|
|
44
|
+
}
|
|
45
|
+
const OPTIONAL_DIRS = [
|
|
46
|
+
{ value: 'scripts', label: 'scripts/', hint: 'Executable code (Python, Bash, JS)' },
|
|
47
|
+
{ value: 'references', label: 'references/', hint: 'Additional documentation' },
|
|
48
|
+
{ value: 'assets', label: 'assets/', hint: 'Templates, images, data files' },
|
|
49
|
+
];
|
|
50
|
+
/**
|
|
51
|
+
* Quote a YAML value if it contains characters that could break parsing.
|
|
52
|
+
* Wraps in double quotes and escapes internal double quotes/backslashes.
|
|
53
|
+
*/
|
|
54
|
+
function yamlQuote(value) {
|
|
55
|
+
// If the value contains any YAML-special characters, quote it
|
|
56
|
+
if (/[:#[\]{}&*!|>'"%@`\n\r\\,]/.test(value) || value.startsWith('-') || value.startsWith('?')) {
|
|
57
|
+
const escaped = value
|
|
58
|
+
.replace(/\\/g, '\\\\')
|
|
59
|
+
.replace(/"/g, '\\"')
|
|
60
|
+
.replace(/\n/g, '\\n')
|
|
61
|
+
.replace(/\r/g, '\\r');
|
|
62
|
+
return `"${escaped}"`;
|
|
63
|
+
}
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Generate SKILL.md content with frontmatter and template.
|
|
68
|
+
*/
|
|
69
|
+
function generateSkillMd(meta, dirs) {
|
|
70
|
+
const frontmatterLines = [
|
|
71
|
+
'---',
|
|
72
|
+
`name: ${yamlQuote(meta.name)}`,
|
|
73
|
+
`description: ${yamlQuote(meta.description)}`,
|
|
74
|
+
];
|
|
75
|
+
if (meta.license) {
|
|
76
|
+
frontmatterLines.push(`license: ${yamlQuote(meta.license)}`);
|
|
77
|
+
}
|
|
78
|
+
if (meta.author || meta.version) {
|
|
79
|
+
frontmatterLines.push('metadata:');
|
|
80
|
+
if (meta.author) {
|
|
81
|
+
frontmatterLines.push(` author: ${yamlQuote(meta.author)}`);
|
|
82
|
+
}
|
|
83
|
+
if (meta.version) {
|
|
84
|
+
frontmatterLines.push(` version: ${yamlQuote(meta.version)}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
frontmatterLines.push('---');
|
|
88
|
+
const sections = [
|
|
89
|
+
`${frontmatterLines.join('\n')}`,
|
|
90
|
+
`# ${meta.name}`,
|
|
91
|
+
meta.description,
|
|
92
|
+
`## Instructions\n\n<!-- Add your skill instructions here. This is what the AI agent will read and follow. -->`,
|
|
93
|
+
`## Examples\n\n<!-- Provide examples of how this skill should be used. -->`,
|
|
94
|
+
];
|
|
95
|
+
if (dirs.includes('references')) {
|
|
96
|
+
sections.push(`## References\n\n<!-- Reference additional docs from the references/ directory. -->\n<!-- Example: See [detailed guide](references/REFERENCE.md) for more info. -->`);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
sections.push(`## References\n\n<!-- Link to documentation, APIs, or other resources the agent might need. -->`);
|
|
100
|
+
}
|
|
101
|
+
if (dirs.includes('scripts')) {
|
|
102
|
+
sections.push(`## Scripts\n\n<!-- Executable code is available in the scripts/ directory. -->\n<!-- Example: Run scripts/setup.sh to configure the environment. -->`);
|
|
103
|
+
}
|
|
104
|
+
return sections.join('\n\n') + '\n';
|
|
105
|
+
}
|
|
106
|
+
// === Command Definition ===
|
|
107
|
+
export const initCommand = new Command('init')
|
|
108
|
+
.description('Create a new skill template')
|
|
109
|
+
.option('--name <name>', 'Skill name (lowercase, hyphens)')
|
|
110
|
+
.option('--description <desc>', 'Skill description')
|
|
111
|
+
.option('--author <author>', 'Skill author')
|
|
112
|
+
.option('--version <version>', 'Skill version (default: 1.0.0)')
|
|
113
|
+
.option('--license <license>', 'Skill license (e.g., MIT, Apache-2.0)')
|
|
114
|
+
.option('-y, --yes', 'Skip confirmation prompts')
|
|
115
|
+
.option('--project', 'Create in current project (./.claude)')
|
|
116
|
+
.option('--global', 'Create in home directory (~/.claude)')
|
|
117
|
+
.helpOption('-h, --help', 'Display help for command')
|
|
118
|
+
.addHelpText('after', `
|
|
119
|
+
Examples:
|
|
120
|
+
$ skillfish init Interactive skill creation
|
|
121
|
+
$ skillfish init --name my-skill Create skill with specified name
|
|
122
|
+
$ skillfish init --project Create in current project
|
|
123
|
+
$ skillfish init --name my-skill --yes Non-interactive creation`)
|
|
124
|
+
.action(async (options, command) => {
|
|
125
|
+
const jsonMode = command.parent?.opts().json ?? false;
|
|
126
|
+
const version = command.parent?.opts().version ?? '0.0.0';
|
|
127
|
+
// JSON output state
|
|
128
|
+
const jsonOutput = {
|
|
129
|
+
success: true,
|
|
130
|
+
errors: [],
|
|
131
|
+
created: [],
|
|
132
|
+
skipped: [],
|
|
133
|
+
};
|
|
134
|
+
function addError(message) {
|
|
135
|
+
jsonOutput.errors.push(message);
|
|
136
|
+
jsonOutput.success = false;
|
|
137
|
+
}
|
|
138
|
+
function outputJsonAndExit(exitCode) {
|
|
139
|
+
jsonOutput.exit_code = exitCode;
|
|
140
|
+
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
141
|
+
process.exit(exitCode);
|
|
142
|
+
}
|
|
143
|
+
function exitWithError(message, exitCode, useClackLog = false) {
|
|
144
|
+
if (jsonMode) {
|
|
145
|
+
addError(message);
|
|
146
|
+
outputJsonAndExit(exitCode);
|
|
147
|
+
}
|
|
148
|
+
if (useClackLog) {
|
|
149
|
+
p.log.error(message);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
console.error(message);
|
|
153
|
+
}
|
|
154
|
+
process.exit(exitCode);
|
|
155
|
+
}
|
|
156
|
+
// Show banner and intro (TTY only, not in JSON mode)
|
|
157
|
+
if (isTTY() && !jsonMode) {
|
|
158
|
+
console.log();
|
|
159
|
+
console.log(pc.cyan(' ≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋'));
|
|
160
|
+
console.log(` ${pc.cyan('><>')} ${pc.bold('SKILL FISH')} ${pc.cyan('><>')}`);
|
|
161
|
+
console.log(pc.cyan(' ≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋'));
|
|
162
|
+
console.log();
|
|
163
|
+
p.intro(`${pc.bgCyan(pc.black(' skillfish '))} ${pc.dim(`v${version}`)} ${pc.dim('· Create a skill')}`);
|
|
164
|
+
}
|
|
165
|
+
const skipPrompts = options.yes ?? false;
|
|
166
|
+
const projectFlag = options.project ?? false;
|
|
167
|
+
const globalFlag = options.global ?? false;
|
|
168
|
+
if (projectFlag && globalFlag) {
|
|
169
|
+
exitWithError('Cannot use both --project and --global. Choose one.', EXIT_CODES.INVALID_ARGS);
|
|
170
|
+
}
|
|
171
|
+
// 1. Get skill name
|
|
172
|
+
let skillName;
|
|
173
|
+
if (options.name) {
|
|
174
|
+
// Validate provided name
|
|
175
|
+
const normalized = normalizeSkillName(options.name);
|
|
176
|
+
if (!isValidSkillName(normalized)) {
|
|
177
|
+
exitWithError('Invalid skill name. Use lowercase letters, numbers, and hyphens (e.g., my-skill).', EXIT_CODES.INVALID_ARGS);
|
|
178
|
+
}
|
|
179
|
+
skillName = normalized;
|
|
180
|
+
}
|
|
181
|
+
else if (!isInputTTY() || jsonMode) {
|
|
182
|
+
exitWithError('Skill name is required. Use --name <name> in non-interactive mode.', EXIT_CODES.INVALID_ARGS);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
// Interactive prompt for skill name
|
|
186
|
+
const nameInput = await p.text({
|
|
187
|
+
message: 'Skill name',
|
|
188
|
+
placeholder: 'my-skill',
|
|
189
|
+
validate: (value) => {
|
|
190
|
+
const normalized = normalizeSkillName(value);
|
|
191
|
+
if (!normalized) {
|
|
192
|
+
return 'Skill name is required';
|
|
193
|
+
}
|
|
194
|
+
if (!isValidSkillName(normalized)) {
|
|
195
|
+
return 'Use lowercase letters, numbers, and hyphens (e.g., my-skill)';
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
if (p.isCancel(nameInput)) {
|
|
200
|
+
p.cancel('Cancelled');
|
|
201
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
202
|
+
}
|
|
203
|
+
skillName = normalizeSkillName(nameInput);
|
|
204
|
+
}
|
|
205
|
+
// 2. Get skill description
|
|
206
|
+
let skillDescription;
|
|
207
|
+
if (options.description) {
|
|
208
|
+
skillDescription = options.description;
|
|
209
|
+
}
|
|
210
|
+
else if (!isInputTTY() || jsonMode) {
|
|
211
|
+
exitWithError('Skill description is required. Use --description <desc> in non-interactive mode.', EXIT_CODES.INVALID_ARGS);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
const descInput = await p.text({
|
|
215
|
+
message: 'Description',
|
|
216
|
+
placeholder: 'What does this skill do?',
|
|
217
|
+
validate: (value) => {
|
|
218
|
+
if (!value.trim()) {
|
|
219
|
+
return 'Description is required';
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
if (p.isCancel(descInput)) {
|
|
224
|
+
p.cancel('Cancelled');
|
|
225
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
226
|
+
}
|
|
227
|
+
skillDescription = descInput.trim();
|
|
228
|
+
}
|
|
229
|
+
// 3. Optional metadata (author, version, license) - only prompt interactively
|
|
230
|
+
let author = options.author;
|
|
231
|
+
let skillVersion = options.version || '1.0.0';
|
|
232
|
+
let license = options.license;
|
|
233
|
+
if (isInputTTY() && !jsonMode && !skipPrompts) {
|
|
234
|
+
const addMetadata = await p.confirm({
|
|
235
|
+
message: 'Add optional metadata (author, license)?',
|
|
236
|
+
initialValue: false,
|
|
237
|
+
});
|
|
238
|
+
if (p.isCancel(addMetadata)) {
|
|
239
|
+
p.cancel('Cancelled');
|
|
240
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
241
|
+
}
|
|
242
|
+
if (addMetadata) {
|
|
243
|
+
const authorInput = await p.text({
|
|
244
|
+
message: 'Author',
|
|
245
|
+
placeholder: 'your-name or your-org (optional)',
|
|
246
|
+
});
|
|
247
|
+
if (p.isCancel(authorInput)) {
|
|
248
|
+
p.cancel('Cancelled');
|
|
249
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
250
|
+
}
|
|
251
|
+
author = authorInput.trim() || undefined;
|
|
252
|
+
const licenseInput = await p.select({
|
|
253
|
+
message: 'License',
|
|
254
|
+
options: [
|
|
255
|
+
{ value: '', label: 'None' },
|
|
256
|
+
{ value: 'MIT', label: 'MIT' },
|
|
257
|
+
{ value: 'Apache-2.0', label: 'Apache-2.0' },
|
|
258
|
+
{ value: 'BSD-3-Clause', label: 'BSD-3-Clause' },
|
|
259
|
+
{ value: 'GPL-3.0', label: 'GPL-3.0' },
|
|
260
|
+
{ value: 'AGPL-3.0', label: 'AGPL-3.0' },
|
|
261
|
+
{ value: 'Unlicense', label: 'Unlicense' },
|
|
262
|
+
],
|
|
263
|
+
});
|
|
264
|
+
if (p.isCancel(licenseInput)) {
|
|
265
|
+
p.cancel('Cancelled');
|
|
266
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
267
|
+
}
|
|
268
|
+
license = licenseInput || undefined;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// 4. Select optional directories
|
|
272
|
+
let optionalDirs = [];
|
|
273
|
+
if (isInputTTY() && !jsonMode && !skipPrompts) {
|
|
274
|
+
const addDirs = await p.confirm({
|
|
275
|
+
message: 'Include optional directories (scripts/, references/, assets/)?',
|
|
276
|
+
initialValue: false,
|
|
277
|
+
});
|
|
278
|
+
if (p.isCancel(addDirs)) {
|
|
279
|
+
p.cancel('Cancelled');
|
|
280
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
281
|
+
}
|
|
282
|
+
if (addDirs) {
|
|
283
|
+
const selected = await p.multiselect({
|
|
284
|
+
message: 'Select directories to include',
|
|
285
|
+
options: OPTIONAL_DIRS.map((d) => ({
|
|
286
|
+
value: d.value,
|
|
287
|
+
label: d.label,
|
|
288
|
+
hint: d.hint,
|
|
289
|
+
})),
|
|
290
|
+
required: false,
|
|
291
|
+
});
|
|
292
|
+
if (p.isCancel(selected)) {
|
|
293
|
+
p.cancel('Cancelled');
|
|
294
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
295
|
+
}
|
|
296
|
+
optionalDirs = selected;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// 5. Determine install location (global vs project)
|
|
300
|
+
const baseDir = await selectInstallLocation(projectFlag, globalFlag, jsonMode);
|
|
301
|
+
// 6. Select agents to create skill for
|
|
302
|
+
const detected = getDetectedAgents();
|
|
303
|
+
if (detected.length === 0) {
|
|
304
|
+
const errorMsg = 'No agents detected. Install Claude Code, Cursor, or another supported agent first.';
|
|
305
|
+
if (jsonMode) {
|
|
306
|
+
addError(errorMsg);
|
|
307
|
+
outputJsonAndExit(EXIT_CODES.GENERAL_ERROR);
|
|
308
|
+
}
|
|
309
|
+
p.log.error(errorMsg);
|
|
310
|
+
p.outro(pc.dim('https://skill.fish/agents'));
|
|
311
|
+
process.exit(EXIT_CODES.GENERAL_ERROR);
|
|
312
|
+
}
|
|
313
|
+
let targetAgents;
|
|
314
|
+
if (!isInputTTY() || jsonMode || skipPrompts) {
|
|
315
|
+
// Non-TTY or JSON mode or --yes: use all detected agents
|
|
316
|
+
if (!jsonMode) {
|
|
317
|
+
console.log(`Creating for ${detected.length} agent(s): ${detected.map((a) => a.name).join(', ')}`);
|
|
318
|
+
}
|
|
319
|
+
targetAgents = detected;
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
// Interactive: let user choose from detected agents
|
|
323
|
+
const isLocal = baseDir !== homedir();
|
|
324
|
+
targetAgents = await selectAgents(detected, isLocal, jsonMode);
|
|
325
|
+
}
|
|
326
|
+
// 7. Create the skill
|
|
327
|
+
const skillMeta = {
|
|
328
|
+
name: skillName,
|
|
329
|
+
description: skillDescription,
|
|
330
|
+
author,
|
|
331
|
+
version: skillVersion,
|
|
332
|
+
license,
|
|
333
|
+
};
|
|
334
|
+
const skillContent = generateSkillMd(skillMeta, optionalDirs);
|
|
335
|
+
const isLocal = baseDir !== homedir();
|
|
336
|
+
const pathPrefix = isLocal ? '.' : '~';
|
|
337
|
+
if (!jsonMode) {
|
|
338
|
+
p.log.step(`Creating ${pc.bold(skillName)}`);
|
|
339
|
+
}
|
|
340
|
+
let created = 0;
|
|
341
|
+
let skipped = 0;
|
|
342
|
+
for (const agent of targetAgents) {
|
|
343
|
+
const skillDir = join(getAgentSkillDir(agent, baseDir), skillName);
|
|
344
|
+
const skillFilePath = join(skillDir, SKILL_FILENAME);
|
|
345
|
+
// Check if skill already exists
|
|
346
|
+
if (existsSync(skillFilePath)) {
|
|
347
|
+
if (!jsonMode) {
|
|
348
|
+
console.log(` ${pc.yellow('●')} ${agent.name} ${pc.dim('(already exists)')}`);
|
|
349
|
+
}
|
|
350
|
+
jsonOutput.skipped.push({
|
|
351
|
+
skill: skillName,
|
|
352
|
+
agent: agent.name,
|
|
353
|
+
reason: 'Already exists',
|
|
354
|
+
});
|
|
355
|
+
skipped++;
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
// Create skill directory and SKILL.md
|
|
359
|
+
try {
|
|
360
|
+
mkdirSync(skillDir, { recursive: true, mode: 0o700 });
|
|
361
|
+
writeFileSync(skillFilePath, skillContent, { mode: 0o600 });
|
|
362
|
+
}
|
|
363
|
+
catch (err) {
|
|
364
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
365
|
+
if (!jsonMode) {
|
|
366
|
+
console.log(` ${pc.red('✗')} ${agent.name} ${pc.dim(`(${errMsg})`)}`);
|
|
367
|
+
}
|
|
368
|
+
addError(`Failed to create skill for ${agent.name}: ${errMsg}`);
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
// Create optional directories (non-fatal — SKILL.md is already written)
|
|
372
|
+
for (const dir of optionalDirs) {
|
|
373
|
+
try {
|
|
374
|
+
mkdirSync(join(skillDir, dir), { recursive: true, mode: 0o700 });
|
|
375
|
+
}
|
|
376
|
+
catch (err) {
|
|
377
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
378
|
+
if (!jsonMode) {
|
|
379
|
+
console.log(` ${pc.yellow('!')} ${agent.name}: failed to create ${dir}/ ${pc.dim(`(${errMsg})`)}`);
|
|
380
|
+
}
|
|
381
|
+
jsonOutput.errors.push(`Failed to create ${dir}/ for ${agent.name}: ${errMsg}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const displayPath = `${pathPrefix}/${agent.dir}/${skillName}`;
|
|
385
|
+
if (!jsonMode) {
|
|
386
|
+
console.log(` ${pc.green('✓')} ${agent.name} ${pc.dim(`→ ${displayPath}`)}`);
|
|
387
|
+
}
|
|
388
|
+
jsonOutput.created.push({
|
|
389
|
+
skill: skillName,
|
|
390
|
+
agent: agent.name,
|
|
391
|
+
path: skillDir,
|
|
392
|
+
});
|
|
393
|
+
created++;
|
|
394
|
+
}
|
|
395
|
+
// Summary
|
|
396
|
+
if (jsonMode) {
|
|
397
|
+
outputJsonAndExit(jsonOutput.success ? EXIT_CODES.SUCCESS : EXIT_CODES.GENERAL_ERROR);
|
|
398
|
+
}
|
|
399
|
+
console.log();
|
|
400
|
+
if (created > 0) {
|
|
401
|
+
p.log.success(`Created ${pc.bold(skillName)}`);
|
|
402
|
+
console.log();
|
|
403
|
+
console.log(pc.dim(' Next steps:'));
|
|
404
|
+
console.log(pc.dim(` 1. Edit ${SKILL_FILENAME} to add your instructions`));
|
|
405
|
+
if (optionalDirs.length > 0) {
|
|
406
|
+
console.log(pc.dim(` 2. Add files to ${optionalDirs.map((d) => `${d}/`).join(', ')}`));
|
|
407
|
+
console.log(pc.dim(' 3. Test with your AI agent'));
|
|
408
|
+
console.log(pc.dim(' 4. Share on skill.fish or GitHub'));
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
console.log(pc.dim(' 2. Test with your AI agent'));
|
|
412
|
+
console.log(pc.dim(' 3. Share on skill.fish or GitHub'));
|
|
413
|
+
}
|
|
414
|
+
console.log();
|
|
415
|
+
p.outro(pc.green(`Done! Created ${created} skill${created === 1 ? '' : 's'}`));
|
|
416
|
+
}
|
|
417
|
+
else if (skipped > 0) {
|
|
418
|
+
p.outro(pc.yellow(`Skill "${skillName}" already exists for all selected agents`));
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
p.outro(pc.yellow('No skills created'));
|
|
422
|
+
}
|
|
423
|
+
process.exit(created > 0 || skipped > 0 ? EXIT_CODES.SUCCESS : EXIT_CODES.GENERAL_ERROR);
|
|
424
|
+
});
|
|
425
|
+
// === Helper Functions ===
|
|
426
|
+
async function selectInstallLocation(projectFlag, globalFlag, jsonMode) {
|
|
427
|
+
// If flag specified, use it
|
|
428
|
+
if (projectFlag) {
|
|
429
|
+
if (!jsonMode) {
|
|
430
|
+
p.log.info(`Location: ${pc.cyan('Project')} ${pc.dim('(current directory)')}`);
|
|
431
|
+
}
|
|
432
|
+
return process.cwd();
|
|
433
|
+
}
|
|
434
|
+
if (globalFlag) {
|
|
435
|
+
if (!jsonMode) {
|
|
436
|
+
p.log.info(`Location: ${pc.cyan('Global')} ${pc.dim('(home directory)')}`);
|
|
437
|
+
}
|
|
438
|
+
return homedir();
|
|
439
|
+
}
|
|
440
|
+
// Non-TTY or JSON mode defaults to project (more common for init)
|
|
441
|
+
if (!isInputTTY() || jsonMode) {
|
|
442
|
+
return process.cwd();
|
|
443
|
+
}
|
|
444
|
+
// Interactive selection
|
|
445
|
+
const location = await p.select({
|
|
446
|
+
message: 'Install location',
|
|
447
|
+
options: [
|
|
448
|
+
{
|
|
449
|
+
value: 'project',
|
|
450
|
+
label: 'Project',
|
|
451
|
+
hint: 'For this project only (recommended)',
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
value: 'global',
|
|
455
|
+
label: 'Global',
|
|
456
|
+
hint: 'Available in all projects',
|
|
457
|
+
},
|
|
458
|
+
],
|
|
459
|
+
});
|
|
460
|
+
if (p.isCancel(location)) {
|
|
461
|
+
p.cancel('Cancelled');
|
|
462
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
463
|
+
}
|
|
464
|
+
return location === 'project' ? process.cwd() : homedir();
|
|
465
|
+
}
|
|
466
|
+
async function selectAgents(agents, isLocal, jsonMode) {
|
|
467
|
+
const pathPrefix = isLocal ? '.' : '~';
|
|
468
|
+
// Show detected agents
|
|
469
|
+
if (!jsonMode) {
|
|
470
|
+
p.log.info(`Detected ${pc.cyan(agents.length.toString())} agent${agents.length === 1 ? '' : 's'}: ${agents.map((a) => a.name).join(', ')}`);
|
|
471
|
+
}
|
|
472
|
+
const installAll = await p.confirm({
|
|
473
|
+
message: 'Create for all detected agents?',
|
|
474
|
+
initialValue: true,
|
|
475
|
+
});
|
|
476
|
+
if (p.isCancel(installAll)) {
|
|
477
|
+
p.cancel('Cancelled');
|
|
478
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
479
|
+
}
|
|
480
|
+
if (installAll) {
|
|
481
|
+
return agents;
|
|
482
|
+
}
|
|
483
|
+
// User wants to choose specific agents
|
|
484
|
+
const options = agents.map((a) => ({
|
|
485
|
+
value: a.name,
|
|
486
|
+
label: a.name,
|
|
487
|
+
hint: `${pathPrefix}/${a.dir}`,
|
|
488
|
+
}));
|
|
489
|
+
const selected = await p.multiselect({
|
|
490
|
+
message: 'Select agents',
|
|
491
|
+
options,
|
|
492
|
+
required: true,
|
|
493
|
+
});
|
|
494
|
+
if (p.isCancel(selected)) {
|
|
495
|
+
p.cancel('Cancelled');
|
|
496
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
497
|
+
}
|
|
498
|
+
return agents.filter((a) => selected.includes(a.name));
|
|
499
|
+
}
|
package/dist/commands/update.js
CHANGED
|
@@ -9,7 +9,7 @@ import pc from 'picocolors';
|
|
|
9
9
|
import { getDetectedAgents, getAgentSkillDir } from '../lib/agents.js';
|
|
10
10
|
import { listInstalledSkillsInDir, installSkill } from '../lib/installer.js';
|
|
11
11
|
import { readManifest } from '../lib/manifest.js';
|
|
12
|
-
import {
|
|
12
|
+
import { fetchRecursiveTree, getSkillSha, RateLimitError, RepoNotFoundError, NetworkError, GitHubApiError, } from '../lib/github.js';
|
|
13
13
|
import { EXIT_CODES } from '../lib/constants.js';
|
|
14
14
|
import { isInputTTY, isTTY } from '../utils.js';
|
|
15
15
|
// === Command Definition ===
|
|
@@ -260,14 +260,15 @@ function collectTrackedSkills(agents) {
|
|
|
260
260
|
}
|
|
261
261
|
/**
|
|
262
262
|
* Check which tracked skills have updates available.
|
|
263
|
-
* Caches tree
|
|
263
|
+
* Caches recursive tree lookups to avoid duplicate API calls for skills from the same repo.
|
|
264
|
+
* Uses directory-level SHA comparison for accurate change detection.
|
|
264
265
|
*/
|
|
265
266
|
async function checkForUpdates(skills) {
|
|
266
267
|
const outdated = [];
|
|
267
268
|
const errors = [];
|
|
268
269
|
let rateLimitHit = false;
|
|
269
|
-
// Cache tree
|
|
270
|
-
const
|
|
270
|
+
// Cache full tree lookups by owner/repo/branch to avoid duplicate API calls
|
|
271
|
+
const treeCache = new Map();
|
|
271
272
|
const errorCache = new Map();
|
|
272
273
|
for (const skill of skills) {
|
|
273
274
|
const cacheKey = `${skill.manifest.owner}/${skill.manifest.repo}/${skill.manifest.branch}`;
|
|
@@ -288,12 +289,13 @@ async function checkForUpdates(skills) {
|
|
|
288
289
|
}
|
|
289
290
|
continue;
|
|
290
291
|
}
|
|
291
|
-
// Check if we already have a cached
|
|
292
|
-
let
|
|
293
|
-
if (!
|
|
292
|
+
// Check if we already have a cached tree for this repo
|
|
293
|
+
let cached = treeCache.get(cacheKey);
|
|
294
|
+
if (!cached) {
|
|
294
295
|
try {
|
|
295
|
-
|
|
296
|
-
|
|
296
|
+
const { sha, tree } = await fetchRecursiveTree(skill.manifest.owner, skill.manifest.repo, skill.manifest.branch);
|
|
297
|
+
cached = { rootSha: sha, tree };
|
|
298
|
+
treeCache.set(cacheKey, cached);
|
|
297
299
|
}
|
|
298
300
|
catch (err) {
|
|
299
301
|
if (err instanceof RateLimitError) {
|
|
@@ -319,6 +321,9 @@ async function checkForUpdates(skills) {
|
|
|
319
321
|
continue;
|
|
320
322
|
}
|
|
321
323
|
}
|
|
324
|
+
// Get directory-specific SHA for accurate change detection
|
|
325
|
+
// Falls back to root SHA for backward compatibility with old manifests
|
|
326
|
+
const remoteSha = getSkillSha(cached.tree, skill.manifest.path) ?? cached.rootSha;
|
|
322
327
|
if (skill.manifest.sha !== remoteSha) {
|
|
323
328
|
outdated.push({ ...skill, remoteSha });
|
|
324
329
|
}
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { fileURLToPath } from 'url';
|
|
|
10
10
|
import { dirname, join } from 'path';
|
|
11
11
|
import updateNotifier from 'update-notifier';
|
|
12
12
|
import { addCommand } from './commands/add.js';
|
|
13
|
+
import { initCommand } from './commands/init.js';
|
|
13
14
|
import { listCommand } from './commands/list.js';
|
|
14
15
|
import { removeCommand } from './commands/remove.js';
|
|
15
16
|
import { updateCommand } from './commands/update.js';
|
|
@@ -37,6 +38,7 @@ const program = new Command()
|
|
|
37
38
|
Examples:
|
|
38
39
|
$ skillfish add owner/repo Install skills from a repository
|
|
39
40
|
$ skillfish add owner/repo/plugin/skill Install a specific skill
|
|
41
|
+
$ skillfish init Create a new skill template
|
|
40
42
|
$ skillfish list Show installed skills
|
|
41
43
|
$ skillfish remove my-skill Remove a skill
|
|
42
44
|
|
|
@@ -47,6 +49,7 @@ program.hook('preAction', (thisCommand) => {
|
|
|
47
49
|
});
|
|
48
50
|
// Add subcommands
|
|
49
51
|
program.addCommand(addCommand);
|
|
52
|
+
program.addCommand(initCommand);
|
|
50
53
|
program.addCommand(listCommand);
|
|
51
54
|
program.addCommand(removeCommand);
|
|
52
55
|
program.addCommand(updateCommand);
|
package/dist/lib/github.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* GitHub API functions for skill discovery and fetching.
|
|
3
3
|
*/
|
|
4
|
+
import { type GitTreeItem } from '../utils.js';
|
|
4
5
|
export declare const SKILL_FILENAME = "SKILL.md";
|
|
5
6
|
/**
|
|
6
7
|
* Result of skill discovery including branch and SHA information.
|
|
@@ -8,9 +9,20 @@ export declare const SKILL_FILENAME = "SKILL.md";
|
|
|
8
9
|
export interface SkillDiscoveryResult {
|
|
9
10
|
paths: string[];
|
|
10
11
|
branch: string;
|
|
11
|
-
/**
|
|
12
|
+
/** Root tree SHA from git/trees response - changes when any file in repo changes */
|
|
12
13
|
sha: string;
|
|
14
|
+
/** Raw tree items for directory-level SHA lookups */
|
|
15
|
+
tree: GitTreeItem[];
|
|
13
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Get the SHA for a skill path from a git tree.
|
|
19
|
+
* - For subdirectory skills: returns the directory's tree SHA
|
|
20
|
+
* - For root-level skills: returns the SKILL.md blob SHA
|
|
21
|
+
*
|
|
22
|
+
* This enables directory-level change detection instead of repo-level,
|
|
23
|
+
* reducing false "outdated" notifications when unrelated files change.
|
|
24
|
+
*/
|
|
25
|
+
export declare function getSkillSha(tree: GitTreeItem[], skillPath: string): string | undefined;
|
|
14
26
|
/**
|
|
15
27
|
* Thrown when GitHub API rate limit is exceeded.
|
|
16
28
|
*/
|
|
@@ -67,6 +79,19 @@ export declare function fetchSkillMdContent(owner: string, repo: string, path: s
|
|
|
67
79
|
* @throws {GitHubApiError} When the API response format is unexpected
|
|
68
80
|
*/
|
|
69
81
|
export declare function fetchTreeSha(owner: string, repo: string, branch: string): Promise<string>;
|
|
82
|
+
/**
|
|
83
|
+
* Fetch the recursive tree for a repository branch.
|
|
84
|
+
* Returns both the root SHA and the full tree for directory-level SHA lookups.
|
|
85
|
+
*
|
|
86
|
+
* @throws {RepoNotFoundError} When the repository is not found
|
|
87
|
+
* @throws {RateLimitError} When GitHub API rate limit is exceeded
|
|
88
|
+
* @throws {NetworkError} On network errors
|
|
89
|
+
* @throws {GitHubApiError} When the API response format is unexpected
|
|
90
|
+
*/
|
|
91
|
+
export declare function fetchRecursiveTree(owner: string, repo: string, branch: string): Promise<{
|
|
92
|
+
sha: string;
|
|
93
|
+
tree: GitTreeItem[];
|
|
94
|
+
}>;
|
|
70
95
|
/**
|
|
71
96
|
* Find all SKILL.md files in a GitHub repository.
|
|
72
97
|
* Fetches the default branch, then searches for skills on that branch.
|
package/dist/lib/github.js
CHANGED
|
@@ -7,6 +7,26 @@ const API_TIMEOUT_MS = 10000;
|
|
|
7
7
|
const MAX_RETRIES = 3;
|
|
8
8
|
const RETRY_DELAYS_MS = [1000, 2000, 4000]; // Exponential backoff
|
|
9
9
|
export const SKILL_FILENAME = 'SKILL.md';
|
|
10
|
+
// === Helper Functions ===
|
|
11
|
+
/**
|
|
12
|
+
* Get the SHA for a skill path from a git tree.
|
|
13
|
+
* - For subdirectory skills: returns the directory's tree SHA
|
|
14
|
+
* - For root-level skills: returns the SKILL.md blob SHA
|
|
15
|
+
*
|
|
16
|
+
* This enables directory-level change detection instead of repo-level,
|
|
17
|
+
* reducing false "outdated" notifications when unrelated files change.
|
|
18
|
+
*/
|
|
19
|
+
export function getSkillSha(tree, skillPath) {
|
|
20
|
+
// Root-level skill - use the blob SHA of SKILL.md itself
|
|
21
|
+
if (skillPath === SKILL_FILENAME) {
|
|
22
|
+
const blob = tree.find((item) => item.path === SKILL_FILENAME && item.type === 'blob');
|
|
23
|
+
return blob?.sha;
|
|
24
|
+
}
|
|
25
|
+
// Subdirectory skill - use the directory's tree SHA
|
|
26
|
+
const dirPath = skillPath.replace(/\/SKILL\.md$/, '');
|
|
27
|
+
const dir = tree.find((item) => item.path === dirPath && item.type === 'tree');
|
|
28
|
+
return dir?.sha;
|
|
29
|
+
}
|
|
10
30
|
// === Error Types ===
|
|
11
31
|
/**
|
|
12
32
|
* Thrown when GitHub API rate limit is exceeded.
|
|
@@ -208,25 +228,25 @@ export async function fetchTreeSha(owner, repo, branch) {
|
|
|
208
228
|
}
|
|
209
229
|
}
|
|
210
230
|
/**
|
|
211
|
-
*
|
|
212
|
-
*
|
|
231
|
+
* Fetch the recursive tree for a repository branch.
|
|
232
|
+
* Returns both the root SHA and the full tree for directory-level SHA lookups.
|
|
213
233
|
*
|
|
214
|
-
* @returns SkillDiscoveryResult with paths and the branch they were found on
|
|
215
|
-
* @throws {RateLimitError} When GitHub API rate limit is exceeded
|
|
216
234
|
* @throws {RepoNotFoundError} When the repository is not found
|
|
217
|
-
* @throws {
|
|
235
|
+
* @throws {RateLimitError} When GitHub API rate limit is exceeded
|
|
236
|
+
* @throws {NetworkError} On network errors
|
|
218
237
|
* @throws {GitHubApiError} When the API response format is unexpected
|
|
219
238
|
*/
|
|
220
|
-
export async function
|
|
239
|
+
export async function fetchRecursiveTree(owner, repo, branch) {
|
|
221
240
|
const headers = { 'User-Agent': 'skillfish' };
|
|
222
|
-
// Get the default branch
|
|
223
|
-
const branch = await fetchDefaultBranch(owner, repo);
|
|
224
241
|
const controller = new AbortController();
|
|
225
242
|
const timeoutId = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
|
|
226
243
|
try {
|
|
227
244
|
const url = `https://api.github.com/repos/${owner}/${repo}/git/trees/${branch}?recursive=1`;
|
|
228
245
|
const res = await fetchWithRetry(url, { headers, signal: controller.signal });
|
|
229
246
|
checkRateLimit(res);
|
|
247
|
+
if (res.status === 404) {
|
|
248
|
+
throw new RepoNotFoundError(owner, repo);
|
|
249
|
+
}
|
|
230
250
|
if (!res.ok) {
|
|
231
251
|
throw new GitHubApiError(`GitHub API returned status ${res.status}`);
|
|
232
252
|
}
|
|
@@ -234,9 +254,12 @@ export async function findAllSkillMdFiles(owner, repo) {
|
|
|
234
254
|
if (!isGitTreeResponse(rawData)) {
|
|
235
255
|
throw new GitHubApiError('Unexpected response format from GitHub API.');
|
|
236
256
|
}
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
257
|
+
const sha = rawData.sha;
|
|
258
|
+
if (typeof sha !== 'string' || !/^[a-f0-9]{40}$/.test(sha)) {
|
|
259
|
+
throw new GitHubApiError('Invalid or missing sha field in tree response');
|
|
260
|
+
}
|
|
261
|
+
const tree = rawData.tree ?? [];
|
|
262
|
+
return { sha, tree };
|
|
240
263
|
}
|
|
241
264
|
catch (err) {
|
|
242
265
|
wrapApiError(err);
|
|
@@ -245,3 +268,22 @@ export async function findAllSkillMdFiles(owner, repo) {
|
|
|
245
268
|
clearTimeout(timeoutId);
|
|
246
269
|
}
|
|
247
270
|
}
|
|
271
|
+
/**
|
|
272
|
+
* Find all SKILL.md files in a GitHub repository.
|
|
273
|
+
* Fetches the default branch, then searches for skills on that branch.
|
|
274
|
+
*
|
|
275
|
+
* @returns SkillDiscoveryResult with paths and the branch they were found on
|
|
276
|
+
* @throws {RateLimitError} When GitHub API rate limit is exceeded
|
|
277
|
+
* @throws {RepoNotFoundError} When the repository is not found
|
|
278
|
+
* @throws {NetworkError} On network errors (timeout, connection refused)
|
|
279
|
+
* @throws {GitHubApiError} When the API response format is unexpected
|
|
280
|
+
*/
|
|
281
|
+
export async function findAllSkillMdFiles(owner, repo) {
|
|
282
|
+
// Get the default branch
|
|
283
|
+
const branch = await fetchDefaultBranch(owner, repo);
|
|
284
|
+
// Fetch the recursive tree (reuses fetchRecursiveTree to avoid duplication)
|
|
285
|
+
const { sha, tree } = await fetchRecursiveTree(owner, repo, branch);
|
|
286
|
+
// Extract SKILL.md paths from the tree
|
|
287
|
+
const paths = extractSkillPaths({ tree }, SKILL_FILENAME);
|
|
288
|
+
return { paths, branch, sha, tree };
|
|
289
|
+
}
|
package/dist/utils.d.ts
CHANGED
|
@@ -133,6 +133,21 @@ export interface UpdateJsonOutput extends BaseJsonOutput {
|
|
|
133
133
|
outdated: OutdatedSkill[];
|
|
134
134
|
updated: InstalledSkill[];
|
|
135
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* JSON output for the `init` command.
|
|
138
|
+
*/
|
|
139
|
+
export interface InitJsonOutput extends BaseJsonOutput {
|
|
140
|
+
created: {
|
|
141
|
+
skill: string;
|
|
142
|
+
agent: string;
|
|
143
|
+
path: string;
|
|
144
|
+
}[];
|
|
145
|
+
skipped: {
|
|
146
|
+
skill: string;
|
|
147
|
+
agent: string;
|
|
148
|
+
reason: string;
|
|
149
|
+
}[];
|
|
150
|
+
}
|
|
136
151
|
/** @deprecated Use AddJsonOutput instead */
|
|
137
152
|
export type JsonOutput = AddJsonOutput;
|
|
138
153
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skillfish",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.17",
|
|
4
4
|
"description": "All in one Skill manager for AI coding agents. Install, update, and sync Skills across Claude Code, Cursor, Copilot + more.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
78
|
"@eslint/js": "^9.39.2",
|
|
79
|
-
"@types/node": "^
|
|
79
|
+
"@types/node": "^25.0.10",
|
|
80
80
|
"@types/update-notifier": "^6.0.8",
|
|
81
81
|
"eslint": "^9.39.2",
|
|
82
82
|
"eslint-config-prettier": "^10.1.8",
|