@yonpark/skillhub-cli 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -185
- package/dist/commands/logout.js +44 -0
- package/dist/commands/status.js +61 -0
- package/dist/commands/sync.js +153 -291
- package/dist/core/syncCore.js +122 -0
- package/dist/index.js +80 -3
- package/dist/service/config.js +26 -0
- package/dist/service/gistService.js +25 -15
- package/dist/service/skillsService.js +197 -0
- package/dist/utils/output.js +10 -0
- package/dist/utils/retry.js +69 -0
- package/package.json +55 -48
package/README.md
CHANGED
|
@@ -1,185 +1,121 @@
|
|
|
1
|
-
# SkillHub CLI
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
```json
|
|
123
|
-
{
|
|
124
|
-
"skills": [
|
|
125
|
-
{ "name": "vercel-composition-patterns", "source": "vercel-labs/agent-skills" },
|
|
126
|
-
{ "name": "my-custom-skill", "source": "yw9142/my-agent-tools" }
|
|
127
|
-
],
|
|
128
|
-
"updatedAt": "2026-01-29T07:27:53.844Z"
|
|
129
|
-
}
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
- `skills`: list of installed skills + where they came from (`owner/repo`)
|
|
133
|
-
- `updatedAt`: ISO timestamp when the payload was last written
|
|
134
|
-
|
|
135
|
-
This keeps the format easy to inspect and edit directly in GitHub if needed.
|
|
136
|
-
|
|
137
|
-
## Typical Workflows
|
|
138
|
-
|
|
139
|
-
### First machine (create backup)
|
|
140
|
-
|
|
141
|
-
```bash
|
|
142
|
-
# 1) Install skills using the official CLI
|
|
143
|
-
npx skills add vercel-labs/agent-skills --all -g -y
|
|
144
|
-
|
|
145
|
-
# 2) Login once
|
|
146
|
-
skillhub login
|
|
147
|
-
|
|
148
|
-
# 3) Push your current skills to Gist
|
|
149
|
-
skillhub sync
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
### New machine (restore from backup)
|
|
153
|
-
|
|
154
|
-
```bash
|
|
155
|
-
# 1) Install and build SkillHub CLI
|
|
156
|
-
npm install
|
|
157
|
-
npm run build
|
|
158
|
-
npm link
|
|
159
|
-
|
|
160
|
-
# 2) Login with the same GitHub account/token
|
|
161
|
-
skillhub login
|
|
162
|
-
|
|
163
|
-
# 3) Pull skills from Gist and install missing ones
|
|
164
|
-
skillhub sync
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
## Notes and Limitations
|
|
168
|
-
|
|
169
|
-
- Local discovery uses `npx skills list -g` as the primary source. That output
|
|
170
|
-
usually does **not** include `owner/repo`, so SkillHub may fall back to a
|
|
171
|
-
default source (`vercel-labs/agent-skills`) unless it can infer a source from
|
|
172
|
-
`skills-lock.json`.
|
|
173
|
-
- For installs, SkillHub uses the stored `source` per skill:
|
|
174
|
-
|
|
175
|
-
```bash
|
|
176
|
-
npx skills add "<owner>/<repo>" --skill "<skill-name>" -g -y
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
If a skill name doesn’t exist in that repo, the install will fail but the
|
|
180
|
-
overall sync will continue.
|
|
181
|
-
- The CLI currently focuses on **global** skills (`skills list -g`),
|
|
182
|
-
not project-scoped skills.
|
|
183
|
-
- Error messages try to surface both:
|
|
184
|
-
- Which step failed (local list / lock file / install / Gist)
|
|
185
|
-
- The underlying CLI output for easier debugging
|
|
1
|
+
# SkillHub CLI
|
|
2
|
+
|
|
3
|
+
Sync local AI agent skills (`npx skills`) with a private GitHub Gist.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
npm run build
|
|
10
|
+
npm link
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Run without linking:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
node bin/skillhub.js <command>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Published package:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx @yw9142/skillhub-cli <command>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Commands
|
|
26
|
+
|
|
27
|
+
### Login
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
skillhub login
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- Prompts for a GitHub PAT (classic, `gist` scope)
|
|
34
|
+
- Verifies token + Gist API access
|
|
35
|
+
- Stores token locally via `conf`
|
|
36
|
+
|
|
37
|
+
### Sync
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
skillhub sync
|
|
41
|
+
skillhub sync --strategy union
|
|
42
|
+
skillhub sync --strategy latest
|
|
43
|
+
skillhub sync --dry-run
|
|
44
|
+
skillhub sync --json
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
- `union`: merge local and remote skills, install missing local skills, upload only when changed
|
|
48
|
+
- `latest`: compare `remote.updatedAt` and local `lastSyncAt`
|
|
49
|
+
- `--dry-run`: compute plan only (no install, no Gist update, no config write)
|
|
50
|
+
- `--json`: single JSON output object
|
|
51
|
+
|
|
52
|
+
### Status
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
skillhub status
|
|
56
|
+
skillhub status --json
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Shows:
|
|
60
|
+
|
|
61
|
+
- login state
|
|
62
|
+
- stored gist id
|
|
63
|
+
- last successful sync timestamp
|
|
64
|
+
- local skill count (if available)
|
|
65
|
+
- remote Gist API accessibility
|
|
66
|
+
|
|
67
|
+
### Logout
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
skillhub logout
|
|
71
|
+
skillhub logout --yes
|
|
72
|
+
skillhub logout --json
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Clears stored session keys: `githubToken`, `gistId`, `lastSyncAt`.
|
|
76
|
+
|
|
77
|
+
## Payload Format
|
|
78
|
+
|
|
79
|
+
`skillhub.json` in Gist:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"skills": [
|
|
84
|
+
{ "name": "vercel-composition-patterns", "source": "vercel-labs/agent-skills" }
|
|
85
|
+
],
|
|
86
|
+
"updatedAt": "2026-01-29T07:27:53.844Z"
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Backward compatibility for legacy `skills: string[]` is preserved.
|
|
91
|
+
|
|
92
|
+
## Local npm Credentials
|
|
93
|
+
|
|
94
|
+
If you need npm auth, copy `.npmrc.example` to `.npmrc` and keep it local only.
|
|
95
|
+
|
|
96
|
+
## Release (Changesets)
|
|
97
|
+
|
|
98
|
+
Create release note:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npm run changeset
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Version packages:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npm run version-packages
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Publish:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
npm run release
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
CI release workflow requires:
|
|
117
|
+
|
|
118
|
+
- `NPM_TOKEN` repository secret for npmjs publish of `@yonpark/skillhub-cli`.
|
|
119
|
+
- GitHub Packages publish uses `GITHUB_TOKEN` by default.
|
|
120
|
+
- If needed, add `GH_PACKAGES_TOKEN` (PAT with `write:packages`) for GitHub Packages.
|
|
121
|
+
- GitHub Packages target package is `@yw9142/skillhub-cli`.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.clearStoredSession = clearStoredSession;
|
|
4
|
+
exports.runLogout = runLogout;
|
|
5
|
+
const config_1 = require("../service/config");
|
|
6
|
+
const output_1 = require("../utils/output");
|
|
7
|
+
const REMOVED_KEYS = ["githubToken", "gistId", "lastSyncAt"];
|
|
8
|
+
function formatLogoutSummary(summary) {
|
|
9
|
+
if (!summary.cleared) {
|
|
10
|
+
return "Logout cancelled.";
|
|
11
|
+
}
|
|
12
|
+
return `Logout completed. Removed keys: ${summary.removedKeys.join(", ")}`;
|
|
13
|
+
}
|
|
14
|
+
async function clearStoredSession(store = config_1.configStore) {
|
|
15
|
+
await store.clearSession();
|
|
16
|
+
return [...REMOVED_KEYS];
|
|
17
|
+
}
|
|
18
|
+
async function runLogout(options = {}) {
|
|
19
|
+
const asJson = options.json === true;
|
|
20
|
+
const force = options.yes === true;
|
|
21
|
+
if (!force) {
|
|
22
|
+
const { default: inquirer } = await import("inquirer");
|
|
23
|
+
const { confirm } = await inquirer.prompt([
|
|
24
|
+
{
|
|
25
|
+
type: "confirm",
|
|
26
|
+
name: "confirm",
|
|
27
|
+
default: false,
|
|
28
|
+
message: "Delete stored GitHub session data (token, gistId, lastSyncAt)?",
|
|
29
|
+
},
|
|
30
|
+
]);
|
|
31
|
+
if (!confirm) {
|
|
32
|
+
const cancelled = { cleared: false, removedKeys: [] };
|
|
33
|
+
(0, output_1.emitOutput)(cancelled, asJson, formatLogoutSummary);
|
|
34
|
+
return cancelled;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const removedKeys = await clearStoredSession();
|
|
38
|
+
const summary = {
|
|
39
|
+
cleared: true,
|
|
40
|
+
removedKeys,
|
|
41
|
+
};
|
|
42
|
+
(0, output_1.emitOutput)(summary, asJson, formatLogoutSummary);
|
|
43
|
+
return summary;
|
|
44
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runStatus = runStatus;
|
|
4
|
+
const config_1 = require("../service/config");
|
|
5
|
+
const gistService_1 = require("../service/gistService");
|
|
6
|
+
const skillsService_1 = require("../service/skillsService");
|
|
7
|
+
const output_1 = require("../utils/output");
|
|
8
|
+
function formatStatusSummary(summary) {
|
|
9
|
+
const lines = [
|
|
10
|
+
`loggedIn=${summary.loggedIn}`,
|
|
11
|
+
`gistId=${summary.gistId ?? "none"}`,
|
|
12
|
+
`lastSyncAt=${summary.lastSyncAt ?? "none"}`,
|
|
13
|
+
`localSkillCount=${summary.localSkillCount === null ? "unavailable" : String(summary.localSkillCount)}`,
|
|
14
|
+
`remoteAccessible=${summary.remoteAccessible === null
|
|
15
|
+
? "unknown"
|
|
16
|
+
: String(summary.remoteAccessible)}`,
|
|
17
|
+
];
|
|
18
|
+
if (summary.errors.length > 0) {
|
|
19
|
+
lines.push(`errors=${summary.errors.length}`);
|
|
20
|
+
for (const error of summary.errors) {
|
|
21
|
+
lines.push(`- ${error}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return lines.join("\n");
|
|
25
|
+
}
|
|
26
|
+
async function runStatus(options = {}) {
|
|
27
|
+
const asJson = options.json === true;
|
|
28
|
+
const token = await config_1.configStore.getToken();
|
|
29
|
+
const gistId = await config_1.configStore.getGistId();
|
|
30
|
+
const lastSyncAt = await config_1.configStore.getLastSyncAt();
|
|
31
|
+
const summary = {
|
|
32
|
+
loggedIn: Boolean(token),
|
|
33
|
+
gistId: gistId ?? null,
|
|
34
|
+
lastSyncAt: lastSyncAt ?? null,
|
|
35
|
+
localSkillCount: null,
|
|
36
|
+
remoteAccessible: token ? null : false,
|
|
37
|
+
errors: [],
|
|
38
|
+
};
|
|
39
|
+
try {
|
|
40
|
+
const localSkills = await (0, skillsService_1.getLocalSkills)();
|
|
41
|
+
summary.localSkillCount = localSkills.length;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
45
|
+
summary.errors.push(`Failed to read local skills: ${message}`);
|
|
46
|
+
}
|
|
47
|
+
if (token) {
|
|
48
|
+
try {
|
|
49
|
+
const octokit = (0, gistService_1.createOctokit)(token);
|
|
50
|
+
await (0, gistService_1.checkGistAccess)(octokit);
|
|
51
|
+
summary.remoteAccessible = true;
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
55
|
+
summary.remoteAccessible = false;
|
|
56
|
+
summary.errors.push(`Failed to access GitHub Gist API: ${message}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
(0, output_1.emitOutput)(summary, asJson, formatStatusSummary);
|
|
60
|
+
return summary;
|
|
61
|
+
}
|