skillfish 1.0.8 → 1.0.10
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 +657 -13
- package/README.md +29 -7
- package/dist/commands/add.js +21 -11
- package/dist/commands/list.js +20 -4
- package/dist/commands/remove.js +6 -5
- package/dist/commands/update.d.ts +5 -0
- package/dist/commands/update.js +327 -0
- package/dist/index.js +2 -0
- package/dist/lib/agents.d.ts +4 -4
- package/dist/lib/github.d.ts +13 -1
- package/dist/lib/github.js +39 -2
- package/dist/lib/installer.d.ts +6 -4
- package/dist/lib/installer.js +15 -2
- package/dist/lib/manifest.d.ts +45 -0
- package/dist/lib/manifest.js +100 -0
- package/dist/utils.d.ts +25 -6
- package/dist/utils.js +4 -6
- package/package.json +32 -3
package/README.md
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/knoxgraeme/skillfish/main/assets/logo.png" alt="skillfish" width="600">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://npmjs.com/package/skillfish"><img src="https://img.shields.io/npm/v/skillfish" alt="npm"></a>
|
|
7
|
+
<a href="https://npmjs.com/package/skillfish"><img src="https://img.shields.io/npm/dm/skillfish" alt="downloads"></a>
|
|
8
|
+
<a href="LICENSE"><img src="https://img.shields.io/npm/l/skillfish" alt="license"></a>
|
|
9
|
+
<a href="package.json"><img src="https://img.shields.io/node/v/skillfish" alt="node"></a>
|
|
10
|
+
<a href="https://github.com/knoxgraeme/skillfish/actions"><img src="https://github.com/knoxgraeme/skillfish/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
11
|
+
</p>
|
|
2
12
|
|
|
3
13
|
Install AI agent skills from GitHub with a single command.
|
|
4
14
|
|
|
@@ -101,9 +111,9 @@ When a repo contains multiple skills, you'll get an interactive multi-select men
|
|
|
101
111
|
|
|
102
112
|
```
|
|
103
113
|
◆ Select skills to install
|
|
104
|
-
│ ◻
|
|
105
|
-
│ ◻
|
|
106
|
-
│ ◻
|
|
114
|
+
│ ◻ my-skill - A helpful skill for your AI agent
|
|
115
|
+
│ ◻ another-skill - Another useful capability
|
|
116
|
+
│ ◻ third-skill - Yet another skill option
|
|
107
117
|
│ ...
|
|
108
118
|
└
|
|
109
119
|
```
|
|
@@ -117,7 +127,7 @@ Use `--all` to install all skills non-interactively (useful for automation).
|
|
|
117
127
|
npx skillfish add user/my-skill
|
|
118
128
|
|
|
119
129
|
# Install using full path from GitHub
|
|
120
|
-
npx skillfish add
|
|
130
|
+
npx skillfish add owner/repo/path/to/skill
|
|
121
131
|
|
|
122
132
|
# Install from a plugin repo with explicit path
|
|
123
133
|
npx skillfish add org/plugin-repo --path plugins/my-plugin/skills/skill-name
|
|
@@ -183,10 +193,22 @@ skillfish add owner/repo --json
|
|
|
183
193
|
# Output includes: "exit_code": 0 (or error code)
|
|
184
194
|
```
|
|
185
195
|
|
|
196
|
+
## Security
|
|
197
|
+
|
|
198
|
+
**Security Note:** Skills are markdown files that provide instructions to AI agents. Always review skills before installing. skillfish does not vet third-party skills.
|
|
199
|
+
|
|
200
|
+
To report security vulnerabilities, please email security@skill.fish. See [SECURITY.md](SECURITY.md) for details.
|
|
201
|
+
|
|
186
202
|
## Contributing
|
|
187
203
|
|
|
188
|
-
Contributions welcome
|
|
204
|
+
Contributions welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
205
|
+
|
|
206
|
+
Please read our [Code of Conduct](CODE_OF_CONDUCT.md) before participating.
|
|
207
|
+
|
|
208
|
+
## Changelog
|
|
209
|
+
|
|
210
|
+
See [CHANGELOG.md](CHANGELOG.md) for release history.
|
|
189
211
|
|
|
190
212
|
## License
|
|
191
213
|
|
|
192
|
-
AGPL-3.0
|
|
214
|
+
[AGPL-3.0](LICENSE) - Graeme Knox
|
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, SKILL_FILENAME, RateLimitError, RepoNotFoundError, NetworkError, GitHubApiError, } from '../lib/github.js';
|
|
12
|
+
import { findAllSkillMdFiles, fetchSkillMdContent, fetchDefaultBranch, fetchTreeSha, 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 ===
|
|
@@ -115,14 +115,23 @@ Examples:
|
|
|
115
115
|
// 1. Discover or select skills
|
|
116
116
|
let discoveryResult;
|
|
117
117
|
if (explicitPath) {
|
|
118
|
-
// For explicit paths, we still need to fetch the default branch for
|
|
118
|
+
// For explicit paths, we still need to fetch the default branch and SHA for tracking
|
|
119
119
|
try {
|
|
120
120
|
const branch = await fetchDefaultBranch(owner, repo);
|
|
121
|
-
|
|
121
|
+
// Fetch tree SHA for manifest tracking
|
|
122
|
+
let sha;
|
|
123
|
+
try {
|
|
124
|
+
sha = await fetchTreeSha(owner, repo, branch);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// If we can't fetch SHA, install without manifest tracking
|
|
128
|
+
sha = undefined;
|
|
129
|
+
}
|
|
130
|
+
discoveryResult = { paths: [explicitPath], branch, sha };
|
|
122
131
|
}
|
|
123
|
-
catch
|
|
132
|
+
catch {
|
|
124
133
|
// If we can't fetch the branch, let degit try its own detection
|
|
125
|
-
discoveryResult = { paths: [explicitPath], branch: undefined };
|
|
134
|
+
discoveryResult = { paths: [explicitPath], branch: undefined, sha: undefined };
|
|
126
135
|
}
|
|
127
136
|
}
|
|
128
137
|
else {
|
|
@@ -134,7 +143,7 @@ Examples:
|
|
|
134
143
|
}
|
|
135
144
|
process.exit(EXIT_CODES.NOT_FOUND);
|
|
136
145
|
}
|
|
137
|
-
const { paths: skillPaths, branch: discoveredBranch } = discoveryResult;
|
|
146
|
+
const { paths: skillPaths, branch: discoveredBranch, sha: discoveredSha } = discoveryResult;
|
|
138
147
|
// 2. Determine install location (global vs project)
|
|
139
148
|
const baseDir = await selectInstallLocation(projectFlag, globalFlag, jsonMode);
|
|
140
149
|
// 3. Select agents to install to
|
|
@@ -193,6 +202,7 @@ Examples:
|
|
|
193
202
|
force,
|
|
194
203
|
baseDir,
|
|
195
204
|
branch: discoveredBranch,
|
|
205
|
+
sha: discoveredSha,
|
|
196
206
|
});
|
|
197
207
|
if (spinner) {
|
|
198
208
|
if (result.failed) {
|
|
@@ -373,7 +383,7 @@ async function discoverSkillPaths(owner, repo, installAll, jsonMode, jsonOutput,
|
|
|
373
383
|
}
|
|
374
384
|
process.exit(exitCode);
|
|
375
385
|
}
|
|
376
|
-
const { paths: skillPaths, branch } = skillDiscovery;
|
|
386
|
+
const { paths: skillPaths, branch, sha } = skillDiscovery;
|
|
377
387
|
if (skillPaths.length === 0) {
|
|
378
388
|
const errorMsg = `No ${SKILL_FILENAME} found in repository`;
|
|
379
389
|
if (jsonMode) {
|
|
@@ -432,7 +442,7 @@ async function discoverSkillPaths(owner, repo, installAll, jsonMode, jsonOutput,
|
|
|
432
442
|
if (!jsonMode) {
|
|
433
443
|
p.log.info(`${pc.bold(displayName)}${desc ? pc.dim(` - ${desc}`) : ''}`);
|
|
434
444
|
}
|
|
435
|
-
return { paths: [matchedSkill.dir], branch };
|
|
445
|
+
return { paths: [matchedSkill.dir], branch, sha };
|
|
436
446
|
}
|
|
437
447
|
// Skill not found - show available skills
|
|
438
448
|
const errorMsg = `Skill "${targetSkillName}" not found in repository`;
|
|
@@ -458,7 +468,7 @@ async function discoverSkillPaths(owner, repo, installAll, jsonMode, jsonOutput,
|
|
|
458
468
|
if (!jsonMode) {
|
|
459
469
|
p.log.info(`${pc.bold(displayName)}${desc ? pc.dim(` - ${desc}`) : ''}`);
|
|
460
470
|
}
|
|
461
|
-
return { paths: [skill.dir], branch };
|
|
471
|
+
return { paths: [skill.dir], branch, sha };
|
|
462
472
|
}
|
|
463
473
|
// Build options for selection with frontmatter metadata
|
|
464
474
|
// Title in label, description in hint (shows on focus)
|
|
@@ -474,7 +484,7 @@ async function discoverSkillPaths(owner, repo, installAll, jsonMode, jsonOutput,
|
|
|
474
484
|
if (!jsonMode) {
|
|
475
485
|
console.log(`Installing all ${skills.length} skills`);
|
|
476
486
|
}
|
|
477
|
-
return { paths: skills.map((s) => s.dir), branch };
|
|
487
|
+
return { paths: skills.map((s) => s.dir), branch, sha };
|
|
478
488
|
}
|
|
479
489
|
// Otherwise, list skills and exit with guidance
|
|
480
490
|
if (jsonMode) {
|
|
@@ -502,7 +512,7 @@ async function discoverSkillPaths(owner, repo, installAll, jsonMode, jsonOutput,
|
|
|
502
512
|
p.cancel('Cancelled');
|
|
503
513
|
process.exit(EXIT_CODES.SUCCESS);
|
|
504
514
|
}
|
|
505
|
-
return { paths: selected, branch };
|
|
515
|
+
return { paths: selected, branch, sha };
|
|
506
516
|
}
|
|
507
517
|
/**
|
|
508
518
|
* SECURITY: Show warning and ask for user confirmation before installing.
|
package/dist/commands/list.js
CHANGED
|
@@ -78,7 +78,12 @@ Examples:
|
|
|
78
78
|
const skillPath = join(globalDir, skill);
|
|
79
79
|
if (!seenPaths.has(skillPath)) {
|
|
80
80
|
seenPaths.add(skillPath);
|
|
81
|
-
const item = {
|
|
81
|
+
const item = {
|
|
82
|
+
agent: agent.name,
|
|
83
|
+
skill,
|
|
84
|
+
path: skillPath,
|
|
85
|
+
location: 'global',
|
|
86
|
+
};
|
|
82
87
|
installed.push(item);
|
|
83
88
|
globalSkills.push(item);
|
|
84
89
|
}
|
|
@@ -92,7 +97,12 @@ Examples:
|
|
|
92
97
|
// Skip if already seen (avoids duplicates when cwd is under home)
|
|
93
98
|
if (!seenPaths.has(skillPath)) {
|
|
94
99
|
seenPaths.add(skillPath);
|
|
95
|
-
const item = {
|
|
100
|
+
const item = {
|
|
101
|
+
agent: agent.name,
|
|
102
|
+
skill,
|
|
103
|
+
path: skillPath,
|
|
104
|
+
location: 'project',
|
|
105
|
+
};
|
|
96
106
|
installed.push(item);
|
|
97
107
|
projectSkills.push(item);
|
|
98
108
|
}
|
|
@@ -136,7 +146,10 @@ Examples:
|
|
|
136
146
|
if (hasBothLocations && isInputTTY()) {
|
|
137
147
|
const locationOptions = [
|
|
138
148
|
{ value: 'global', label: `Global (~/) ${pc.dim(`(${globalSkills.length})`)}` },
|
|
139
|
-
{
|
|
149
|
+
{
|
|
150
|
+
value: 'project',
|
|
151
|
+
label: `Project (./) ${pc.dim(`(${projectSkills.length})`)}`,
|
|
152
|
+
},
|
|
140
153
|
];
|
|
141
154
|
const selectedLocation = await p.select({
|
|
142
155
|
message: 'Select location',
|
|
@@ -208,7 +221,10 @@ Examples:
|
|
|
208
221
|
if (hasBothLocations) {
|
|
209
222
|
const locationOptions = [
|
|
210
223
|
{ value: 'global', label: `Global (~/) ${pc.dim(`(${globalSkills.length})`)}` },
|
|
211
|
-
{
|
|
224
|
+
{
|
|
225
|
+
value: 'project',
|
|
226
|
+
label: `Project (./) ${pc.dim(`(${projectSkills.length})`)}`,
|
|
227
|
+
},
|
|
212
228
|
];
|
|
213
229
|
const selectedLocation = await p.select({
|
|
214
230
|
message: 'Select location',
|
package/dist/commands/remove.js
CHANGED
|
@@ -82,16 +82,14 @@ Examples:
|
|
|
82
82
|
// Detect agents
|
|
83
83
|
const detected = getDetectedAgents();
|
|
84
84
|
if (detected.length === 0) {
|
|
85
|
-
exitWithError('No agents detected. Install Claude Code, Cursor, or another supported agent first.', EXIT_CODES.GENERAL_ERROR, true
|
|
86
|
-
);
|
|
85
|
+
exitWithError('No agents detected. Install Claude Code, Cursor, or another supported agent first.', EXIT_CODES.GENERAL_ERROR, true);
|
|
87
86
|
}
|
|
88
87
|
// Filter to target agent if specified
|
|
89
88
|
let targetAgents = detected;
|
|
90
89
|
if (targetAgentName) {
|
|
91
90
|
const found = detected.filter((a) => a.name.toLowerCase() === targetAgentName.toLowerCase());
|
|
92
91
|
if (found.length === 0) {
|
|
93
|
-
exitWithError(`Agent "${targetAgentName}" not found. Detected agents: ${detected.map((a) => a.name).join(', ')}`, EXIT_CODES.NOT_FOUND, true
|
|
94
|
-
);
|
|
92
|
+
exitWithError(`Agent "${targetAgentName}" not found. Detected agents: ${detected.map((a) => a.name).join(', ')}`, EXIT_CODES.NOT_FOUND, true);
|
|
95
93
|
}
|
|
96
94
|
targetAgents = found;
|
|
97
95
|
}
|
|
@@ -220,7 +218,10 @@ Examples:
|
|
|
220
218
|
if (hasBothLocations) {
|
|
221
219
|
const locationOptions = [
|
|
222
220
|
{ value: 'global', label: `Global (~/) ${pc.dim(`(${globalSkills.length})`)}` },
|
|
223
|
-
{
|
|
221
|
+
{
|
|
222
|
+
value: 'project',
|
|
223
|
+
label: `Project (./) ${pc.dim(`(${projectSkills.length})`)}`,
|
|
224
|
+
},
|
|
224
225
|
];
|
|
225
226
|
const selectedLocation = await p.select({
|
|
226
227
|
message: 'Select location',
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `skillfish update` command - Check for and apply updates to 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, installSkill } from '../lib/installer.js';
|
|
11
|
+
import { readManifest } from '../lib/manifest.js';
|
|
12
|
+
import { fetchTreeSha, RateLimitError, RepoNotFoundError, NetworkError, GitHubApiError, } from '../lib/github.js';
|
|
13
|
+
import { EXIT_CODES } from '../lib/constants.js';
|
|
14
|
+
import { isInputTTY, isTTY } from '../utils.js';
|
|
15
|
+
// === Command Definition ===
|
|
16
|
+
export const updateCommand = new Command('update')
|
|
17
|
+
.description('Check for and apply updates to installed skills')
|
|
18
|
+
.option('-y, --yes', 'Update all outdated skills without prompting')
|
|
19
|
+
.helpOption('-h, --help', 'Display help for command')
|
|
20
|
+
.addHelpText('after', `
|
|
21
|
+
Examples:
|
|
22
|
+
$ skillfish update Check for updates interactively
|
|
23
|
+
$ skillfish update --yes Update all outdated skills
|
|
24
|
+
$ skillfish update --json Check for updates (JSON output)
|
|
25
|
+
$ skillfish update --yes --json Update all outdated skills (JSON output)`)
|
|
26
|
+
.action(async (options, command) => {
|
|
27
|
+
const jsonMode = command.parent?.opts().json ?? false;
|
|
28
|
+
const autoUpdate = options.yes ?? false;
|
|
29
|
+
const version = command.parent?.opts().version ?? '0.0.0';
|
|
30
|
+
// JSON output state
|
|
31
|
+
const jsonOutput = {
|
|
32
|
+
success: true,
|
|
33
|
+
exit_code: EXIT_CODES.SUCCESS,
|
|
34
|
+
errors: [],
|
|
35
|
+
outdated: [],
|
|
36
|
+
updated: [],
|
|
37
|
+
};
|
|
38
|
+
function addError(message) {
|
|
39
|
+
jsonOutput.errors.push(message);
|
|
40
|
+
jsonOutput.success = false;
|
|
41
|
+
}
|
|
42
|
+
function outputJsonAndExit(exitCode) {
|
|
43
|
+
jsonOutput.exit_code = exitCode;
|
|
44
|
+
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
45
|
+
process.exit(exitCode);
|
|
46
|
+
}
|
|
47
|
+
function exitWithError(message, exitCode) {
|
|
48
|
+
if (jsonMode) {
|
|
49
|
+
addError(message);
|
|
50
|
+
outputJsonAndExit(exitCode);
|
|
51
|
+
}
|
|
52
|
+
p.log.error(message);
|
|
53
|
+
process.exit(exitCode);
|
|
54
|
+
}
|
|
55
|
+
// Show banner (TTY only, not in JSON mode)
|
|
56
|
+
if (isTTY() && !jsonMode) {
|
|
57
|
+
console.log();
|
|
58
|
+
console.log(pc.cyan(' ≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋'));
|
|
59
|
+
console.log(` ${pc.cyan('><>')} ${pc.bold('SKILL FISH')} ${pc.cyan('><>')}`);
|
|
60
|
+
console.log(pc.cyan(' ≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋'));
|
|
61
|
+
console.log();
|
|
62
|
+
p.intro(`${pc.bgCyan(pc.black(' skillfish '))} ${pc.dim(`v${version}`)}`);
|
|
63
|
+
}
|
|
64
|
+
// Detect agents
|
|
65
|
+
const detected = getDetectedAgents();
|
|
66
|
+
if (detected.length === 0) {
|
|
67
|
+
exitWithError('No agents detected. Install Claude Code, Cursor, or another supported agent first.', EXIT_CODES.GENERAL_ERROR);
|
|
68
|
+
}
|
|
69
|
+
// Show checking spinner
|
|
70
|
+
let checkSpinner = null;
|
|
71
|
+
if (!jsonMode) {
|
|
72
|
+
checkSpinner = p.spinner();
|
|
73
|
+
checkSpinner.start('Checking for updates...');
|
|
74
|
+
}
|
|
75
|
+
// Collect all tracked skills (skills with manifests)
|
|
76
|
+
const trackedSkills = collectTrackedSkills(detected);
|
|
77
|
+
if (trackedSkills.length === 0) {
|
|
78
|
+
if (checkSpinner) {
|
|
79
|
+
checkSpinner.stop(pc.yellow('No tracked skills found'));
|
|
80
|
+
}
|
|
81
|
+
if (jsonMode) {
|
|
82
|
+
outputJsonAndExit(EXIT_CODES.SUCCESS);
|
|
83
|
+
}
|
|
84
|
+
console.log();
|
|
85
|
+
p.log.info(pc.dim("Skills installed before this version don't have tracking info."));
|
|
86
|
+
p.log.info(pc.dim('Reinstall skills with `skillfish add` to enable updates.'));
|
|
87
|
+
p.outro(pc.dim('Done'));
|
|
88
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
89
|
+
}
|
|
90
|
+
// Check for updates
|
|
91
|
+
const { outdated, errors, rateLimitHit } = await checkForUpdates(trackedSkills);
|
|
92
|
+
if (checkSpinner) {
|
|
93
|
+
if (rateLimitHit) {
|
|
94
|
+
checkSpinner.stop(pc.yellow('Rate limit reached'));
|
|
95
|
+
}
|
|
96
|
+
else if (outdated.length > 0) {
|
|
97
|
+
checkSpinner.stop(`Found ${pc.cyan(outdated.length.toString())} outdated skill${outdated.length === 1 ? '' : 's'}`);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
checkSpinner.stop(pc.green('All skills are up to date'));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Add any errors to output
|
|
104
|
+
for (const error of errors) {
|
|
105
|
+
addError(error);
|
|
106
|
+
}
|
|
107
|
+
// Build outdated skills for JSON output
|
|
108
|
+
jsonOutput.outdated = outdated.map((s) => ({
|
|
109
|
+
skill: s.skill,
|
|
110
|
+
agent: s.agent.name,
|
|
111
|
+
path: s.path,
|
|
112
|
+
location: s.location,
|
|
113
|
+
localSha: s.manifest.sha,
|
|
114
|
+
remoteSha: s.remoteSha,
|
|
115
|
+
source: `${s.manifest.owner}/${s.manifest.repo}`,
|
|
116
|
+
}));
|
|
117
|
+
// If rate limit hit, report and exit
|
|
118
|
+
if (rateLimitHit) {
|
|
119
|
+
if (jsonMode) {
|
|
120
|
+
outputJsonAndExit(EXIT_CODES.NETWORK_ERROR);
|
|
121
|
+
}
|
|
122
|
+
p.log.warn('GitHub API rate limit exceeded. Try again later.');
|
|
123
|
+
process.exit(EXIT_CODES.NETWORK_ERROR);
|
|
124
|
+
}
|
|
125
|
+
// No updates available
|
|
126
|
+
if (outdated.length === 0) {
|
|
127
|
+
if (jsonMode) {
|
|
128
|
+
outputJsonAndExit(EXIT_CODES.SUCCESS);
|
|
129
|
+
}
|
|
130
|
+
p.outro(pc.green('All skills are up to date'));
|
|
131
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
132
|
+
}
|
|
133
|
+
// Display outdated skills (TTY mode)
|
|
134
|
+
if (!jsonMode) {
|
|
135
|
+
console.log();
|
|
136
|
+
for (const skill of outdated) {
|
|
137
|
+
const locationLabel = skill.location === 'global' ? pc.dim('global') : pc.dim('project');
|
|
138
|
+
const shortLocal = skill.manifest.sha.substring(0, 7);
|
|
139
|
+
const shortRemote = skill.remoteSha.substring(0, 7);
|
|
140
|
+
console.log(` ${pc.yellow('•')} ${pc.bold(skill.skill)} (${skill.agent.name}, ${locationLabel})`);
|
|
141
|
+
console.log(` ${pc.dim(shortLocal)} → ${pc.cyan(shortRemote)}`);
|
|
142
|
+
}
|
|
143
|
+
console.log();
|
|
144
|
+
}
|
|
145
|
+
// If --json without --yes: check mode only - don't apply updates
|
|
146
|
+
if (jsonMode && !autoUpdate) {
|
|
147
|
+
outputJsonAndExit(EXIT_CODES.SUCCESS);
|
|
148
|
+
}
|
|
149
|
+
// Prompt for confirmation (unless --yes)
|
|
150
|
+
if (!autoUpdate && isInputTTY() && !jsonMode) {
|
|
151
|
+
const proceed = await p.confirm({
|
|
152
|
+
message: `Update all ${outdated.length} skill${outdated.length === 1 ? '' : 's'}?`,
|
|
153
|
+
initialValue: true,
|
|
154
|
+
});
|
|
155
|
+
if (p.isCancel(proceed) || !proceed) {
|
|
156
|
+
p.cancel('Cancelled');
|
|
157
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Apply updates
|
|
161
|
+
let updatedCount = 0;
|
|
162
|
+
let failedCount = 0;
|
|
163
|
+
for (let i = 0; i < outdated.length; i++) {
|
|
164
|
+
const skill = outdated[i];
|
|
165
|
+
const progress = `[${i + 1}/${outdated.length}]`;
|
|
166
|
+
let updateSpinner = null;
|
|
167
|
+
if (!jsonMode) {
|
|
168
|
+
updateSpinner = p.spinner();
|
|
169
|
+
updateSpinner.start(`${progress} Updating ${skill.skill}...`);
|
|
170
|
+
}
|
|
171
|
+
const result = await installSkill(skill.manifest.owner, skill.manifest.repo, skill.manifest.path, skill.skill, [skill.agent], {
|
|
172
|
+
force: true,
|
|
173
|
+
baseDir: skill.location === 'global' ? homedir() : process.cwd(),
|
|
174
|
+
branch: skill.manifest.branch,
|
|
175
|
+
sha: skill.remoteSha,
|
|
176
|
+
});
|
|
177
|
+
if (result.failed) {
|
|
178
|
+
failedCount++;
|
|
179
|
+
if (updateSpinner) {
|
|
180
|
+
updateSpinner.stop(pc.red(`${progress} ${skill.skill} failed`));
|
|
181
|
+
}
|
|
182
|
+
addError(`Failed to update ${skill.skill}: ${result.failureReason}`);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
updatedCount++;
|
|
186
|
+
if (updateSpinner) {
|
|
187
|
+
updateSpinner.stop(pc.green(`${progress} ${skill.skill} updated`));
|
|
188
|
+
}
|
|
189
|
+
jsonOutput.updated.push({
|
|
190
|
+
skill: skill.skill,
|
|
191
|
+
agent: skill.agent.name,
|
|
192
|
+
path: skill.path,
|
|
193
|
+
location: skill.location,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// Summary
|
|
198
|
+
if (jsonMode) {
|
|
199
|
+
const exitCode = failedCount > 0 ? EXIT_CODES.GENERAL_ERROR : EXIT_CODES.SUCCESS;
|
|
200
|
+
outputJsonAndExit(exitCode);
|
|
201
|
+
}
|
|
202
|
+
console.log();
|
|
203
|
+
if (failedCount > 0) {
|
|
204
|
+
p.outro(pc.yellow(`Updated ${updatedCount} of ${outdated.length} skill${outdated.length === 1 ? '' : 's'}`));
|
|
205
|
+
process.exit(EXIT_CODES.GENERAL_ERROR);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
p.outro(pc.green(`Updated ${updatedCount} skill${updatedCount === 1 ? '' : 's'} successfully`));
|
|
209
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
// === Helper Functions ===
|
|
213
|
+
/**
|
|
214
|
+
* Collect all installed skills that have manifests (tracked skills).
|
|
215
|
+
*/
|
|
216
|
+
function collectTrackedSkills(agents) {
|
|
217
|
+
const tracked = [];
|
|
218
|
+
const seenPaths = new Set();
|
|
219
|
+
for (const agent of agents) {
|
|
220
|
+
// Check global skills
|
|
221
|
+
const globalDir = getAgentSkillDir(agent, homedir());
|
|
222
|
+
const globalSkills = listInstalledSkillsInDir(globalDir);
|
|
223
|
+
for (const skill of globalSkills) {
|
|
224
|
+
const skillPath = join(globalDir, skill);
|
|
225
|
+
if (seenPaths.has(skillPath))
|
|
226
|
+
continue;
|
|
227
|
+
seenPaths.add(skillPath);
|
|
228
|
+
const manifest = readManifest(skillPath);
|
|
229
|
+
if (manifest) {
|
|
230
|
+
tracked.push({
|
|
231
|
+
skill,
|
|
232
|
+
agent,
|
|
233
|
+
path: skillPath,
|
|
234
|
+
location: 'global',
|
|
235
|
+
manifest,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// Check project skills
|
|
240
|
+
const projectDir = getAgentSkillDir(agent, process.cwd());
|
|
241
|
+
const projectSkills = listInstalledSkillsInDir(projectDir);
|
|
242
|
+
for (const skill of projectSkills) {
|
|
243
|
+
const skillPath = join(projectDir, skill);
|
|
244
|
+
if (seenPaths.has(skillPath))
|
|
245
|
+
continue;
|
|
246
|
+
seenPaths.add(skillPath);
|
|
247
|
+
const manifest = readManifest(skillPath);
|
|
248
|
+
if (manifest) {
|
|
249
|
+
tracked.push({
|
|
250
|
+
skill,
|
|
251
|
+
agent,
|
|
252
|
+
path: skillPath,
|
|
253
|
+
location: 'project',
|
|
254
|
+
manifest,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return tracked;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Check which tracked skills have updates available.
|
|
263
|
+
* Caches tree SHA lookups to avoid duplicate API calls for skills from the same repo.
|
|
264
|
+
*/
|
|
265
|
+
async function checkForUpdates(skills) {
|
|
266
|
+
const outdated = [];
|
|
267
|
+
const errors = [];
|
|
268
|
+
let rateLimitHit = false;
|
|
269
|
+
// Cache tree SHA lookups by owner/repo/branch to avoid duplicate API calls
|
|
270
|
+
const shaCache = new Map();
|
|
271
|
+
const errorCache = new Map();
|
|
272
|
+
for (const skill of skills) {
|
|
273
|
+
const cacheKey = `${skill.manifest.owner}/${skill.manifest.repo}/${skill.manifest.branch}`;
|
|
274
|
+
// Check if we already have a cached error for this repo
|
|
275
|
+
const cachedError = errorCache.get(cacheKey);
|
|
276
|
+
if (cachedError) {
|
|
277
|
+
if (cachedError instanceof RepoNotFoundError) {
|
|
278
|
+
errors.push(`${skill.skill}: Repository not found (${skill.manifest.owner}/${skill.manifest.repo})`);
|
|
279
|
+
}
|
|
280
|
+
else if (cachedError instanceof NetworkError) {
|
|
281
|
+
errors.push(`${skill.skill}: ${cachedError.message}`);
|
|
282
|
+
}
|
|
283
|
+
else if (cachedError instanceof GitHubApiError) {
|
|
284
|
+
errors.push(`${skill.skill}: ${cachedError.message}`);
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
errors.push(`${skill.skill}: ${cachedError.message}`);
|
|
288
|
+
}
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
// Check if we already have a cached SHA for this repo
|
|
292
|
+
let remoteSha = shaCache.get(cacheKey);
|
|
293
|
+
if (!remoteSha) {
|
|
294
|
+
try {
|
|
295
|
+
remoteSha = await fetchTreeSha(skill.manifest.owner, skill.manifest.repo, skill.manifest.branch);
|
|
296
|
+
shaCache.set(cacheKey, remoteSha);
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
if (err instanceof RateLimitError) {
|
|
300
|
+
rateLimitHit = true;
|
|
301
|
+
break; // Stop checking on rate limit
|
|
302
|
+
}
|
|
303
|
+
// Cache the error for other skills from the same repo
|
|
304
|
+
if (err instanceof Error) {
|
|
305
|
+
errorCache.set(cacheKey, err);
|
|
306
|
+
}
|
|
307
|
+
if (err instanceof RepoNotFoundError) {
|
|
308
|
+
errors.push(`${skill.skill}: Repository not found (${skill.manifest.owner}/${skill.manifest.repo})`);
|
|
309
|
+
}
|
|
310
|
+
else if (err instanceof NetworkError) {
|
|
311
|
+
errors.push(`${skill.skill}: ${err.message}`);
|
|
312
|
+
}
|
|
313
|
+
else if (err instanceof GitHubApiError) {
|
|
314
|
+
errors.push(`${skill.skill}: ${err.message}`);
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
errors.push(`${skill.skill}: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
|
318
|
+
}
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (skill.manifest.sha !== remoteSha) {
|
|
323
|
+
outdated.push({ ...skill, remoteSha });
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return { outdated, errors, rateLimitHit };
|
|
327
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { dirname, join } from 'path';
|
|
|
11
11
|
import { addCommand } from './commands/add.js';
|
|
12
12
|
import { listCommand } from './commands/list.js';
|
|
13
13
|
import { removeCommand } from './commands/remove.js';
|
|
14
|
+
import { updateCommand } from './commands/update.js';
|
|
14
15
|
// Read version from package.json (single source of truth)
|
|
15
16
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
17
|
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
@@ -45,6 +46,7 @@ program.hook('preAction', (thisCommand) => {
|
|
|
45
46
|
program.addCommand(addCommand);
|
|
46
47
|
program.addCommand(listCommand);
|
|
47
48
|
program.addCommand(removeCommand);
|
|
49
|
+
program.addCommand(updateCommand);
|
|
48
50
|
// Handle --json flag for help output
|
|
49
51
|
program.on('option:json', () => {
|
|
50
52
|
// JSON mode is handled by commands
|
package/dist/lib/agents.d.ts
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
* Agent configuration - data-driven for easier maintenance.
|
|
7
7
|
* Detection checks home directory (agent installed globally) and cwd (local project).
|
|
8
8
|
*/
|
|
9
|
-
export
|
|
9
|
+
export interface AgentConfig {
|
|
10
10
|
readonly name: string;
|
|
11
11
|
readonly dir: string;
|
|
12
12
|
readonly homePaths: readonly string[];
|
|
13
13
|
readonly cwdPaths: readonly string[];
|
|
14
|
-
}
|
|
14
|
+
}
|
|
15
15
|
export declare const AGENT_CONFIGS: readonly AgentConfig[];
|
|
16
16
|
/**
|
|
17
17
|
* Check if an agent is detected on the system.
|
|
@@ -21,11 +21,11 @@ export declare function detectAgent(config: AgentConfig, baseDir?: string): bool
|
|
|
21
21
|
/**
|
|
22
22
|
* Runtime agent type with detect function.
|
|
23
23
|
*/
|
|
24
|
-
export
|
|
24
|
+
export interface Agent {
|
|
25
25
|
readonly name: string;
|
|
26
26
|
readonly dir: string;
|
|
27
27
|
readonly detect: () => boolean;
|
|
28
|
-
}
|
|
28
|
+
}
|
|
29
29
|
/**
|
|
30
30
|
* Build AGENTS array from config (preserves existing API).
|
|
31
31
|
*/
|
package/dist/lib/github.d.ts
CHANGED
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export declare const SKILL_FILENAME = "SKILL.md";
|
|
5
5
|
/**
|
|
6
|
-
* Result of skill discovery including branch information.
|
|
6
|
+
* Result of skill discovery including branch and SHA information.
|
|
7
7
|
*/
|
|
8
8
|
export interface SkillDiscoveryResult {
|
|
9
9
|
paths: string[];
|
|
10
10
|
branch: string;
|
|
11
|
+
/** Tree SHA from git/trees response - changes when any file in repo changes */
|
|
12
|
+
sha: string;
|
|
11
13
|
}
|
|
12
14
|
/**
|
|
13
15
|
* Thrown when GitHub API rate limit is exceeded.
|
|
@@ -55,6 +57,16 @@ export declare function fetchWithRetry(url: string, options: RequestInit, maxRet
|
|
|
55
57
|
* Uses raw.githubusercontent.com which is not rate-limited like the API.
|
|
56
58
|
*/
|
|
57
59
|
export declare function fetchSkillMdContent(owner: string, repo: string, path: string, branch: string): Promise<string | null>;
|
|
60
|
+
/**
|
|
61
|
+
* Fetch the tree SHA for a repository branch.
|
|
62
|
+
* Used for update checks - compares stored SHA with current SHA.
|
|
63
|
+
*
|
|
64
|
+
* @throws {RepoNotFoundError} When the repository is not found
|
|
65
|
+
* @throws {RateLimitError} When GitHub API rate limit is exceeded
|
|
66
|
+
* @throws {NetworkError} On network errors
|
|
67
|
+
* @throws {GitHubApiError} When the API response format is unexpected
|
|
68
|
+
*/
|
|
69
|
+
export declare function fetchTreeSha(owner: string, repo: string, branch: string): Promise<string>;
|
|
58
70
|
/**
|
|
59
71
|
* Find all SKILL.md files in a GitHub repository.
|
|
60
72
|
* Fetches the default branch, then searches for skills on that branch.
|