@yonpark/skillhub-cli 0.2.0 → 0.3.1
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 +32 -34
- package/dist/commands/sync.js +384 -58
- package/dist/core/syncCore.js +54 -31
- package/dist/index.js +81 -61
- package/dist/service/skillsService.js +106 -26
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,55 +24,51 @@ npx @yw9142/skillhub-cli <command>
|
|
|
24
24
|
|
|
25
25
|
## Commands
|
|
26
26
|
|
|
27
|
-
###
|
|
27
|
+
### Auth
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
|
-
skillhub login
|
|
30
|
+
skillhub auth login
|
|
31
|
+
skillhub auth status
|
|
32
|
+
skillhub auth status --json
|
|
33
|
+
skillhub auth logout
|
|
34
|
+
skillhub auth logout --yes
|
|
35
|
+
skillhub auth logout --json
|
|
31
36
|
```
|
|
32
37
|
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
38
|
+
- `auth login`: prompts for a GitHub PAT (classic, `gist` scope), verifies access, and stores it via `conf`
|
|
39
|
+
- `auth status`: shows login state, gist id, last successful sync timestamp, local skill count, and Gist API accessibility
|
|
40
|
+
- `auth logout`: clears stored session keys (`githubToken`, `gistId`, `lastSyncAt`)
|
|
36
41
|
|
|
37
42
|
### Sync
|
|
38
43
|
|
|
44
|
+
`skillhub sync` requires a subcommand.
|
|
45
|
+
|
|
39
46
|
```bash
|
|
40
|
-
skillhub sync
|
|
41
|
-
skillhub sync
|
|
42
|
-
skillhub sync
|
|
43
|
-
skillhub sync
|
|
44
|
-
skillhub sync --json
|
|
47
|
+
skillhub sync pull
|
|
48
|
+
skillhub sync push
|
|
49
|
+
skillhub sync merge
|
|
50
|
+
skillhub sync auto
|
|
45
51
|
```
|
|
46
52
|
|
|
47
|
-
|
|
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
|
+
Common options:
|
|
53
54
|
|
|
54
55
|
```bash
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
--dry-run # compute plan only (no install/remove/upload/config write)
|
|
57
|
+
--json # single JSON output object
|
|
57
58
|
```
|
|
58
59
|
|
|
59
|
-
|
|
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
|
|
60
|
+
`pull` only:
|
|
68
61
|
|
|
69
62
|
```bash
|
|
70
|
-
|
|
71
|
-
skillhub logout --yes
|
|
72
|
-
skillhub logout --json
|
|
63
|
+
--yes # skip deletion confirmation prompt
|
|
73
64
|
```
|
|
74
65
|
|
|
75
|
-
|
|
66
|
+
Mode behavior:
|
|
67
|
+
|
|
68
|
+
- `pull`: mirror remote to local (remote -> local). Installs missing local skills and removes extra local skills.
|
|
69
|
+
- `push`: mirror local to remote (local -> remote). Updates/creates remote Gist payload from local skills.
|
|
70
|
+
- `merge`: union local + remote, installs missing local skills, uploads when remote differs.
|
|
71
|
+
- `auto`: compare `remote.updatedAt` and local `lastSyncAt`; install from remote when remote is newer, otherwise upload local when needed.
|
|
76
72
|
|
|
77
73
|
## Payload Format
|
|
78
74
|
|
|
@@ -87,7 +83,7 @@ Clears stored session keys: `githubToken`, `gistId`, `lastSyncAt`.
|
|
|
87
83
|
}
|
|
88
84
|
```
|
|
89
85
|
|
|
90
|
-
Backward compatibility for legacy `skills: string[]` is preserved.
|
|
86
|
+
Backward compatibility for legacy `skills: string[]` payloads is preserved.
|
|
91
87
|
|
|
92
88
|
## Local npm Credentials
|
|
93
89
|
|
|
@@ -113,9 +109,11 @@ Publish:
|
|
|
113
109
|
npm run release
|
|
114
110
|
```
|
|
115
111
|
|
|
116
|
-
CI release workflow
|
|
112
|
+
CI release workflow publishes to GitHub Packages only:
|
|
117
113
|
|
|
118
|
-
- `NPM_TOKEN` repository secret for npmjs publish of `@yonpark/skillhub-cli`.
|
|
119
114
|
- GitHub Packages publish uses `GITHUB_TOKEN` by default.
|
|
120
115
|
- If needed, add `GH_PACKAGES_TOKEN` (PAT with `write:packages`) for GitHub Packages.
|
|
121
116
|
- GitHub Packages target package is `@yw9142/skillhub-cli`.
|
|
117
|
+
|
|
118
|
+
npmjs publish is manual.
|
|
119
|
+
|
package/dist/commands/sync.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.runSyncMerge = runSyncMerge;
|
|
4
|
+
exports.runSyncAuto = runSyncAuto;
|
|
5
|
+
exports.runSyncPush = runSyncPush;
|
|
6
|
+
exports.runSyncPull = runSyncPull;
|
|
4
7
|
const syncCore_1 = require("../core/syncCore");
|
|
5
8
|
const config_1 = require("../service/config");
|
|
6
9
|
const gistService_1 = require("../service/gistService");
|
|
@@ -12,11 +15,11 @@ function formatSyncSummary(summary) {
|
|
|
12
15
|
? ` (${summary.failed.length} failed - check logs or JSON output)`
|
|
13
16
|
: "";
|
|
14
17
|
const actionLine = summary.dryRun
|
|
15
|
-
? `${prefix}: would upload ${summary.uploaded} change(s), would install ${summary.installPlanned} skill(s)`
|
|
16
|
-
: `${prefix}: uploaded ${summary.uploaded} change(s), installed ${summary.installed} skill(s)`;
|
|
18
|
+
? `${prefix} ${summary.mode}: would upload ${summary.uploaded} change(s), would install ${summary.installPlanned} skill(s), would remove ${summary.removePlanned} skill(s)`
|
|
19
|
+
: `${prefix} ${summary.mode}: uploaded ${summary.uploaded} change(s), installed ${summary.installed} skill(s), removed ${summary.removed} skill(s)`;
|
|
17
20
|
const details = [
|
|
18
21
|
actionLine + failurePart,
|
|
19
|
-
`
|
|
22
|
+
`mode=${summary.mode}`,
|
|
20
23
|
`gistFound=${summary.gistFound}`,
|
|
21
24
|
`gistCreated=${summary.gistCreated}`,
|
|
22
25
|
`remoteNewer=${summary.remoteNewer === null ? "n/a" : String(summary.remoteNewer)}`,
|
|
@@ -24,7 +27,49 @@ function formatSyncSummary(summary) {
|
|
|
24
27
|
];
|
|
25
28
|
return details.join("\n");
|
|
26
29
|
}
|
|
27
|
-
|
|
30
|
+
function createSummary(params) {
|
|
31
|
+
return {
|
|
32
|
+
ok: params.failed.length === 0,
|
|
33
|
+
mode: params.mode,
|
|
34
|
+
dryRun: params.dryRun,
|
|
35
|
+
gistFound: params.gistFound,
|
|
36
|
+
gistCreated: params.gistCreated,
|
|
37
|
+
remoteNewer: params.remoteNewer,
|
|
38
|
+
uploaded: params.uploaded,
|
|
39
|
+
installPlanned: params.installPlanned,
|
|
40
|
+
installed: params.installed,
|
|
41
|
+
removePlanned: params.removePlanned,
|
|
42
|
+
removed: params.removed,
|
|
43
|
+
failed: params.failed,
|
|
44
|
+
lastSyncAtUpdated: params.lastSyncAtUpdated,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function finalizeWithFailures(summary, asJson) {
|
|
48
|
+
if (summary.failed.length === 0) {
|
|
49
|
+
return summary;
|
|
50
|
+
}
|
|
51
|
+
if (asJson) {
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
return summary;
|
|
54
|
+
}
|
|
55
|
+
throw new Error(`Sync ${summary.mode} completed with ${summary.failed.length} failed operation(s). Check logs above.`);
|
|
56
|
+
}
|
|
57
|
+
async function ensureToken() {
|
|
58
|
+
const token = await config_1.configStore.getToken();
|
|
59
|
+
if (!token) {
|
|
60
|
+
throw new Error("You must login first. Run `skillhub auth login` and try again.");
|
|
61
|
+
}
|
|
62
|
+
return token;
|
|
63
|
+
}
|
|
64
|
+
async function safeGetPayload(octokit, gistId) {
|
|
65
|
+
try {
|
|
66
|
+
return await (0, gistService_1.getSkillhubPayload)(octokit, gistId);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function resolveRemoteState(token) {
|
|
28
73
|
const octokit = (0, gistService_1.createOctokit)(token);
|
|
29
74
|
let gistId = await config_1.configStore.getGistId();
|
|
30
75
|
let remotePayload = null;
|
|
@@ -45,52 +90,166 @@ async function resolveRemotePayload(token) {
|
|
|
45
90
|
return {
|
|
46
91
|
octokit,
|
|
47
92
|
gistId,
|
|
48
|
-
|
|
93
|
+
gistFound: Boolean(gistId),
|
|
94
|
+
remotePayload,
|
|
49
95
|
};
|
|
50
96
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return await (0, gistService_1.getSkillhubPayload)(octokit, gistId);
|
|
54
|
-
}
|
|
55
|
-
catch {
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
97
|
+
function asPlanPayload(payload) {
|
|
98
|
+
return payload ?? { skills: [], updatedAt: "" };
|
|
58
99
|
}
|
|
59
|
-
function
|
|
100
|
+
function splitInstallCandidates(candidates) {
|
|
101
|
+
const invalidInstallCandidates = candidates
|
|
102
|
+
.filter((skill) => !(0, skillsService_1.isValidSource)(skill.source))
|
|
103
|
+
.map((skill) => ({
|
|
104
|
+
skill,
|
|
105
|
+
reason: `Invalid source "${skill.source}". Expected owner/repo format.`,
|
|
106
|
+
}));
|
|
107
|
+
const validInstallCandidates = candidates.filter((skill) => (0, skillsService_1.isValidSource)(skill.source));
|
|
60
108
|
return {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
dryRun: params.dryRun,
|
|
64
|
-
gistFound: params.gistFound,
|
|
65
|
-
gistCreated: params.gistCreated,
|
|
66
|
-
remoteNewer: params.remoteNewer,
|
|
67
|
-
uploaded: params.uploaded,
|
|
68
|
-
installPlanned: params.installPlanned,
|
|
69
|
-
installed: params.installed,
|
|
70
|
-
failed: params.failed,
|
|
71
|
-
lastSyncAtUpdated: params.lastSyncAtUpdated,
|
|
109
|
+
invalidInstallCandidates,
|
|
110
|
+
validInstallCandidates,
|
|
72
111
|
};
|
|
73
112
|
}
|
|
74
|
-
async function
|
|
75
|
-
|
|
113
|
+
async function confirmPullRemovalsIfNeeded(removeCandidates, options) {
|
|
114
|
+
if (removeCandidates.length === 0 || options.yes === true) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const { default: inquirer } = await import("inquirer");
|
|
118
|
+
const { confirm } = await inquirer.prompt([
|
|
119
|
+
{
|
|
120
|
+
type: "confirm",
|
|
121
|
+
name: "confirm",
|
|
122
|
+
default: false,
|
|
123
|
+
message: `Pull sync will remove ${removeCandidates.length} local skill(s). Continue?`,
|
|
124
|
+
},
|
|
125
|
+
]);
|
|
126
|
+
if (!confirm) {
|
|
127
|
+
throw new Error("Sync pull cancelled.");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async function runSyncMerge(options = {}) {
|
|
76
131
|
const dryRun = options.dryRun === true;
|
|
77
132
|
const asJson = options.json === true;
|
|
78
|
-
const token = await
|
|
79
|
-
|
|
80
|
-
|
|
133
|
+
const token = await ensureToken();
|
|
134
|
+
const nowIso = new Date().toISOString();
|
|
135
|
+
const localSkills = await (0, skillsService_1.getLocalSkills)();
|
|
136
|
+
const localPayload = {
|
|
137
|
+
skills: localSkills,
|
|
138
|
+
updatedAt: nowIso,
|
|
139
|
+
};
|
|
140
|
+
const { octokit, gistId, gistFound, remotePayload } = await resolveRemoteState(token);
|
|
141
|
+
if (!gistFound) {
|
|
142
|
+
if (dryRun) {
|
|
143
|
+
const summary = createSummary({
|
|
144
|
+
mode: "merge",
|
|
145
|
+
dryRun: true,
|
|
146
|
+
gistFound: false,
|
|
147
|
+
gistCreated: false,
|
|
148
|
+
remoteNewer: null,
|
|
149
|
+
uploaded: 1,
|
|
150
|
+
installPlanned: 0,
|
|
151
|
+
installed: 0,
|
|
152
|
+
removePlanned: 0,
|
|
153
|
+
removed: 0,
|
|
154
|
+
failed: [],
|
|
155
|
+
lastSyncAtUpdated: false,
|
|
156
|
+
});
|
|
157
|
+
(0, output_1.emitOutput)(summary, asJson, formatSyncSummary);
|
|
158
|
+
return summary;
|
|
159
|
+
}
|
|
160
|
+
const created = await (0, gistService_1.createSkillhubGist)(octokit, localPayload);
|
|
161
|
+
if (!created.id) {
|
|
162
|
+
throw new Error("Gist was created, but the ID could not be determined.");
|
|
163
|
+
}
|
|
164
|
+
await config_1.configStore.setGistId(created.id);
|
|
165
|
+
await config_1.configStore.setLastSyncAt(nowIso);
|
|
166
|
+
const summary = createSummary({
|
|
167
|
+
mode: "merge",
|
|
168
|
+
dryRun: false,
|
|
169
|
+
gistFound: false,
|
|
170
|
+
gistCreated: true,
|
|
171
|
+
remoteNewer: null,
|
|
172
|
+
uploaded: 1,
|
|
173
|
+
installPlanned: 0,
|
|
174
|
+
installed: 0,
|
|
175
|
+
removePlanned: 0,
|
|
176
|
+
removed: 0,
|
|
177
|
+
failed: [],
|
|
178
|
+
lastSyncAtUpdated: true,
|
|
179
|
+
});
|
|
180
|
+
(0, output_1.emitOutput)(summary, asJson, formatSyncSummary);
|
|
181
|
+
return summary;
|
|
81
182
|
}
|
|
183
|
+
const plan = (0, syncCore_1.buildMergePlan)({
|
|
184
|
+
localPayload,
|
|
185
|
+
remotePayload: asPlanPayload(remotePayload),
|
|
186
|
+
nowIso,
|
|
187
|
+
});
|
|
188
|
+
const { invalidInstallCandidates, validInstallCandidates } = splitInstallCandidates(plan.installCandidates);
|
|
189
|
+
if (dryRun) {
|
|
190
|
+
const summary = createSummary({
|
|
191
|
+
mode: "merge",
|
|
192
|
+
dryRun: true,
|
|
193
|
+
gistFound: true,
|
|
194
|
+
gistCreated: false,
|
|
195
|
+
remoteNewer: null,
|
|
196
|
+
uploaded: plan.uploadPayload ? 1 : 0,
|
|
197
|
+
installPlanned: plan.installCandidates.length,
|
|
198
|
+
installed: 0,
|
|
199
|
+
removePlanned: 0,
|
|
200
|
+
removed: 0,
|
|
201
|
+
failed: invalidInstallCandidates,
|
|
202
|
+
lastSyncAtUpdated: false,
|
|
203
|
+
});
|
|
204
|
+
(0, output_1.emitOutput)(summary, asJson, formatSyncSummary);
|
|
205
|
+
return summary;
|
|
206
|
+
}
|
|
207
|
+
const installResult = await (0, skillsService_1.installSkills)(validInstallCandidates, {
|
|
208
|
+
verbose: !asJson,
|
|
209
|
+
});
|
|
210
|
+
const failed = [
|
|
211
|
+
...invalidInstallCandidates,
|
|
212
|
+
...installResult.failed,
|
|
213
|
+
];
|
|
214
|
+
if (plan.uploadPayload) {
|
|
215
|
+
await (0, gistService_1.updateSkillhubGist)(octokit, gistId, plan.uploadPayload);
|
|
216
|
+
}
|
|
217
|
+
const summary = createSummary({
|
|
218
|
+
mode: "merge",
|
|
219
|
+
dryRun: false,
|
|
220
|
+
gistFound: true,
|
|
221
|
+
gistCreated: false,
|
|
222
|
+
remoteNewer: null,
|
|
223
|
+
uploaded: plan.uploadPayload ? 1 : 0,
|
|
224
|
+
installPlanned: plan.installCandidates.length,
|
|
225
|
+
installed: installResult.succeeded.length,
|
|
226
|
+
removePlanned: 0,
|
|
227
|
+
removed: 0,
|
|
228
|
+
failed,
|
|
229
|
+
lastSyncAtUpdated: false,
|
|
230
|
+
});
|
|
231
|
+
if (failed.length === 0) {
|
|
232
|
+
await config_1.configStore.setLastSyncAt(nowIso);
|
|
233
|
+
summary.lastSyncAtUpdated = true;
|
|
234
|
+
}
|
|
235
|
+
(0, output_1.emitOutput)(summary, asJson, formatSyncSummary);
|
|
236
|
+
return finalizeWithFailures(summary, asJson);
|
|
237
|
+
}
|
|
238
|
+
async function runSyncAuto(options = {}) {
|
|
239
|
+
const dryRun = options.dryRun === true;
|
|
240
|
+
const asJson = options.json === true;
|
|
241
|
+
const token = await ensureToken();
|
|
82
242
|
const nowIso = new Date().toISOString();
|
|
83
243
|
const localSkills = await (0, skillsService_1.getLocalSkills)();
|
|
84
244
|
const localPayload = {
|
|
85
245
|
skills: localSkills,
|
|
86
246
|
updatedAt: nowIso,
|
|
87
247
|
};
|
|
88
|
-
const { octokit, gistId, remotePayload } = await
|
|
89
|
-
|
|
90
|
-
if (!hasRemoteGist) {
|
|
248
|
+
const { octokit, gistId, gistFound, remotePayload } = await resolveRemoteState(token);
|
|
249
|
+
if (!gistFound) {
|
|
91
250
|
if (dryRun) {
|
|
92
|
-
const summary =
|
|
93
|
-
|
|
251
|
+
const summary = createSummary({
|
|
252
|
+
mode: "auto",
|
|
94
253
|
dryRun: true,
|
|
95
254
|
gistFound: false,
|
|
96
255
|
gistCreated: false,
|
|
@@ -98,6 +257,8 @@ async function runSync(options = {}) {
|
|
|
98
257
|
uploaded: 1,
|
|
99
258
|
installPlanned: 0,
|
|
100
259
|
installed: 0,
|
|
260
|
+
removePlanned: 0,
|
|
261
|
+
removed: 0,
|
|
101
262
|
failed: [],
|
|
102
263
|
lastSyncAtUpdated: false,
|
|
103
264
|
});
|
|
@@ -110,8 +271,8 @@ async function runSync(options = {}) {
|
|
|
110
271
|
}
|
|
111
272
|
await config_1.configStore.setGistId(created.id);
|
|
112
273
|
await config_1.configStore.setLastSyncAt(nowIso);
|
|
113
|
-
const summary =
|
|
114
|
-
|
|
274
|
+
const summary = createSummary({
|
|
275
|
+
mode: "auto",
|
|
115
276
|
dryRun: false,
|
|
116
277
|
gistFound: false,
|
|
117
278
|
gistCreated: true,
|
|
@@ -119,6 +280,8 @@ async function runSync(options = {}) {
|
|
|
119
280
|
uploaded: 1,
|
|
120
281
|
installPlanned: 0,
|
|
121
282
|
installed: 0,
|
|
283
|
+
removePlanned: 0,
|
|
284
|
+
removed: 0,
|
|
122
285
|
failed: [],
|
|
123
286
|
lastSyncAtUpdated: true,
|
|
124
287
|
});
|
|
@@ -126,30 +289,25 @@ async function runSync(options = {}) {
|
|
|
126
289
|
return summary;
|
|
127
290
|
}
|
|
128
291
|
const lastSyncAt = await config_1.configStore.getLastSyncAt();
|
|
129
|
-
const plan = (0, syncCore_1.
|
|
130
|
-
strategy,
|
|
292
|
+
const plan = (0, syncCore_1.buildAutoPlan)({
|
|
131
293
|
localPayload,
|
|
132
|
-
remotePayload,
|
|
294
|
+
remotePayload: asPlanPayload(remotePayload),
|
|
133
295
|
lastSyncAt,
|
|
134
296
|
nowIso,
|
|
135
297
|
});
|
|
136
|
-
const invalidInstallCandidates = plan.installCandidates
|
|
137
|
-
.filter((skill) => !(0, skillsService_1.isValidSource)(skill.source))
|
|
138
|
-
.map((skill) => ({
|
|
139
|
-
skill,
|
|
140
|
-
reason: `Invalid source "${skill.source}". Expected owner/repo format.`,
|
|
141
|
-
}));
|
|
142
|
-
const validInstallCandidates = plan.installCandidates.filter((skill) => (0, skillsService_1.isValidSource)(skill.source));
|
|
298
|
+
const { invalidInstallCandidates, validInstallCandidates } = splitInstallCandidates(plan.installCandidates);
|
|
143
299
|
if (dryRun) {
|
|
144
|
-
const summary =
|
|
145
|
-
|
|
300
|
+
const summary = createSummary({
|
|
301
|
+
mode: "auto",
|
|
146
302
|
dryRun: true,
|
|
147
303
|
gistFound: true,
|
|
148
304
|
gistCreated: false,
|
|
149
|
-
remoteNewer:
|
|
305
|
+
remoteNewer: plan.isRemoteNewer,
|
|
150
306
|
uploaded: plan.uploadPayload ? 1 : 0,
|
|
151
307
|
installPlanned: plan.installCandidates.length,
|
|
152
308
|
installed: 0,
|
|
309
|
+
removePlanned: 0,
|
|
310
|
+
removed: 0,
|
|
153
311
|
failed: invalidInstallCandidates,
|
|
154
312
|
lastSyncAtUpdated: false,
|
|
155
313
|
});
|
|
@@ -159,19 +317,24 @@ async function runSync(options = {}) {
|
|
|
159
317
|
const installResult = await (0, skillsService_1.installSkills)(validInstallCandidates, {
|
|
160
318
|
verbose: !asJson,
|
|
161
319
|
});
|
|
162
|
-
const failed = [
|
|
320
|
+
const failed = [
|
|
321
|
+
...invalidInstallCandidates,
|
|
322
|
+
...installResult.failed,
|
|
323
|
+
];
|
|
163
324
|
if (plan.uploadPayload) {
|
|
164
325
|
await (0, gistService_1.updateSkillhubGist)(octokit, gistId, plan.uploadPayload);
|
|
165
326
|
}
|
|
166
|
-
const summary =
|
|
167
|
-
|
|
327
|
+
const summary = createSummary({
|
|
328
|
+
mode: "auto",
|
|
168
329
|
dryRun: false,
|
|
169
330
|
gistFound: true,
|
|
170
331
|
gistCreated: false,
|
|
171
|
-
remoteNewer:
|
|
332
|
+
remoteNewer: plan.isRemoteNewer,
|
|
172
333
|
uploaded: plan.uploadPayload ? 1 : 0,
|
|
173
334
|
installPlanned: plan.installCandidates.length,
|
|
174
335
|
installed: installResult.succeeded.length,
|
|
336
|
+
removePlanned: 0,
|
|
337
|
+
removed: 0,
|
|
175
338
|
failed,
|
|
176
339
|
lastSyncAtUpdated: false,
|
|
177
340
|
});
|
|
@@ -180,12 +343,175 @@ async function runSync(options = {}) {
|
|
|
180
343
|
summary.lastSyncAtUpdated = true;
|
|
181
344
|
}
|
|
182
345
|
(0, output_1.emitOutput)(summary, asJson, formatSyncSummary);
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
346
|
+
return finalizeWithFailures(summary, asJson);
|
|
347
|
+
}
|
|
348
|
+
async function runSyncPush(options = {}) {
|
|
349
|
+
const dryRun = options.dryRun === true;
|
|
350
|
+
const asJson = options.json === true;
|
|
351
|
+
const token = await ensureToken();
|
|
352
|
+
const nowIso = new Date().toISOString();
|
|
353
|
+
const localSkills = await (0, skillsService_1.getLocalSkills)();
|
|
354
|
+
const localPayload = {
|
|
355
|
+
skills: localSkills,
|
|
356
|
+
updatedAt: nowIso,
|
|
357
|
+
};
|
|
358
|
+
const { octokit, gistId, gistFound, remotePayload } = await resolveRemoteState(token);
|
|
359
|
+
if (!gistFound) {
|
|
360
|
+
if (dryRun) {
|
|
361
|
+
const summary = createSummary({
|
|
362
|
+
mode: "push",
|
|
363
|
+
dryRun: true,
|
|
364
|
+
gistFound: false,
|
|
365
|
+
gistCreated: false,
|
|
366
|
+
remoteNewer: null,
|
|
367
|
+
uploaded: 1,
|
|
368
|
+
installPlanned: 0,
|
|
369
|
+
installed: 0,
|
|
370
|
+
removePlanned: 0,
|
|
371
|
+
removed: 0,
|
|
372
|
+
failed: [],
|
|
373
|
+
lastSyncAtUpdated: false,
|
|
374
|
+
});
|
|
375
|
+
(0, output_1.emitOutput)(summary, asJson, formatSyncSummary);
|
|
186
376
|
return summary;
|
|
187
377
|
}
|
|
188
|
-
|
|
378
|
+
const created = await (0, gistService_1.createSkillhubGist)(octokit, localPayload);
|
|
379
|
+
if (!created.id) {
|
|
380
|
+
throw new Error("Gist was created, but the ID could not be determined.");
|
|
381
|
+
}
|
|
382
|
+
await config_1.configStore.setGistId(created.id);
|
|
383
|
+
await config_1.configStore.setLastSyncAt(nowIso);
|
|
384
|
+
const summary = createSummary({
|
|
385
|
+
mode: "push",
|
|
386
|
+
dryRun: false,
|
|
387
|
+
gistFound: false,
|
|
388
|
+
gistCreated: true,
|
|
389
|
+
remoteNewer: null,
|
|
390
|
+
uploaded: 1,
|
|
391
|
+
installPlanned: 0,
|
|
392
|
+
installed: 0,
|
|
393
|
+
removePlanned: 0,
|
|
394
|
+
removed: 0,
|
|
395
|
+
failed: [],
|
|
396
|
+
lastSyncAtUpdated: true,
|
|
397
|
+
});
|
|
398
|
+
(0, output_1.emitOutput)(summary, asJson, formatSyncSummary);
|
|
399
|
+
return summary;
|
|
189
400
|
}
|
|
401
|
+
const plan = (0, syncCore_1.buildPushPlan)({
|
|
402
|
+
localPayload,
|
|
403
|
+
remotePayload: asPlanPayload(remotePayload),
|
|
404
|
+
nowIso,
|
|
405
|
+
});
|
|
406
|
+
if (dryRun) {
|
|
407
|
+
const summary = createSummary({
|
|
408
|
+
mode: "push",
|
|
409
|
+
dryRun: true,
|
|
410
|
+
gistFound: true,
|
|
411
|
+
gistCreated: false,
|
|
412
|
+
remoteNewer: null,
|
|
413
|
+
uploaded: plan.uploadPayload ? 1 : 0,
|
|
414
|
+
installPlanned: 0,
|
|
415
|
+
installed: 0,
|
|
416
|
+
removePlanned: 0,
|
|
417
|
+
removed: 0,
|
|
418
|
+
failed: [],
|
|
419
|
+
lastSyncAtUpdated: false,
|
|
420
|
+
});
|
|
421
|
+
(0, output_1.emitOutput)(summary, asJson, formatSyncSummary);
|
|
422
|
+
return summary;
|
|
423
|
+
}
|
|
424
|
+
if (plan.uploadPayload) {
|
|
425
|
+
await (0, gistService_1.updateSkillhubGist)(octokit, gistId, plan.uploadPayload);
|
|
426
|
+
}
|
|
427
|
+
await config_1.configStore.setLastSyncAt(nowIso);
|
|
428
|
+
const summary = createSummary({
|
|
429
|
+
mode: "push",
|
|
430
|
+
dryRun: false,
|
|
431
|
+
gistFound: true,
|
|
432
|
+
gistCreated: false,
|
|
433
|
+
remoteNewer: null,
|
|
434
|
+
uploaded: plan.uploadPayload ? 1 : 0,
|
|
435
|
+
installPlanned: 0,
|
|
436
|
+
installed: 0,
|
|
437
|
+
removePlanned: 0,
|
|
438
|
+
removed: 0,
|
|
439
|
+
failed: [],
|
|
440
|
+
lastSyncAtUpdated: true,
|
|
441
|
+
});
|
|
442
|
+
(0, output_1.emitOutput)(summary, asJson, formatSyncSummary);
|
|
190
443
|
return summary;
|
|
191
444
|
}
|
|
445
|
+
async function runSyncPull(options = {}) {
|
|
446
|
+
const dryRun = options.dryRun === true;
|
|
447
|
+
const asJson = options.json === true;
|
|
448
|
+
const token = await ensureToken();
|
|
449
|
+
const nowIso = new Date().toISOString();
|
|
450
|
+
const localSkills = await (0, skillsService_1.getLocalSkills)();
|
|
451
|
+
const localPayload = {
|
|
452
|
+
skills: localSkills,
|
|
453
|
+
updatedAt: nowIso,
|
|
454
|
+
};
|
|
455
|
+
const { gistFound, remotePayload } = await resolveRemoteState(token);
|
|
456
|
+
if (!gistFound) {
|
|
457
|
+
throw new Error("Remote SkillHub Gist not found. Run `skillhub sync push` to create it first.");
|
|
458
|
+
}
|
|
459
|
+
if (!remotePayload) {
|
|
460
|
+
throw new Error("Remote SkillHub payload is missing or invalid. Fix the remote `skillhub.json` and retry.");
|
|
461
|
+
}
|
|
462
|
+
const plan = (0, syncCore_1.buildPullPlan)({
|
|
463
|
+
localPayload,
|
|
464
|
+
remotePayload,
|
|
465
|
+
});
|
|
466
|
+
const { invalidInstallCandidates, validInstallCandidates } = splitInstallCandidates(plan.installCandidates);
|
|
467
|
+
if (dryRun) {
|
|
468
|
+
const summary = createSummary({
|
|
469
|
+
mode: "pull",
|
|
470
|
+
dryRun: true,
|
|
471
|
+
gistFound: true,
|
|
472
|
+
gistCreated: false,
|
|
473
|
+
remoteNewer: null,
|
|
474
|
+
uploaded: 0,
|
|
475
|
+
installPlanned: plan.installCandidates.length,
|
|
476
|
+
installed: 0,
|
|
477
|
+
removePlanned: plan.removeCandidates.length,
|
|
478
|
+
removed: 0,
|
|
479
|
+
failed: invalidInstallCandidates,
|
|
480
|
+
lastSyncAtUpdated: false,
|
|
481
|
+
});
|
|
482
|
+
(0, output_1.emitOutput)(summary, asJson, formatSyncSummary);
|
|
483
|
+
return summary;
|
|
484
|
+
}
|
|
485
|
+
await confirmPullRemovalsIfNeeded(plan.removeCandidates, options);
|
|
486
|
+
const installResult = await (0, skillsService_1.installSkills)(validInstallCandidates, {
|
|
487
|
+
verbose: !asJson,
|
|
488
|
+
});
|
|
489
|
+
const removeResult = await (0, skillsService_1.removeSkills)(plan.removeCandidates, {
|
|
490
|
+
verbose: !asJson,
|
|
491
|
+
});
|
|
492
|
+
const failed = [
|
|
493
|
+
...invalidInstallCandidates,
|
|
494
|
+
...installResult.failed,
|
|
495
|
+
...removeResult.failed,
|
|
496
|
+
];
|
|
497
|
+
const summary = createSummary({
|
|
498
|
+
mode: "pull",
|
|
499
|
+
dryRun: false,
|
|
500
|
+
gistFound: true,
|
|
501
|
+
gistCreated: false,
|
|
502
|
+
remoteNewer: null,
|
|
503
|
+
uploaded: 0,
|
|
504
|
+
installPlanned: plan.installCandidates.length,
|
|
505
|
+
installed: installResult.succeeded.length,
|
|
506
|
+
removePlanned: plan.removeCandidates.length,
|
|
507
|
+
removed: removeResult.succeeded.length,
|
|
508
|
+
failed,
|
|
509
|
+
lastSyncAtUpdated: false,
|
|
510
|
+
});
|
|
511
|
+
if (failed.length === 0) {
|
|
512
|
+
await config_1.configStore.setLastSyncAt(nowIso);
|
|
513
|
+
summary.lastSyncAtUpdated = true;
|
|
514
|
+
}
|
|
515
|
+
(0, output_1.emitOutput)(summary, asJson, formatSyncSummary);
|
|
516
|
+
return finalizeWithFailures(summary, asJson);
|
|
517
|
+
}
|
package/dist/core/syncCore.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.parseStrategy = parseStrategy;
|
|
4
3
|
exports.parseTimestamp = parseTimestamp;
|
|
5
4
|
exports.normalizeSkills = normalizeSkills;
|
|
6
5
|
exports.uniqueSortedSkills = uniqueSortedSkills;
|
|
7
6
|
exports.areSameSkills = areSameSkills;
|
|
8
|
-
exports.
|
|
7
|
+
exports.buildMergePlan = buildMergePlan;
|
|
8
|
+
exports.buildAutoPlan = buildAutoPlan;
|
|
9
|
+
exports.buildPullPlan = buildPullPlan;
|
|
10
|
+
exports.buildPushPlan = buildPushPlan;
|
|
9
11
|
const DEFAULT_SKILL_SOURCE_REPO = "vercel-labs/agent-skills";
|
|
10
12
|
const BANNED_SKILL_NAME_SUBSTRINGS = [
|
|
11
13
|
"No global skills found",
|
|
@@ -13,15 +15,6 @@ const BANNED_SKILL_NAME_SUBSTRINGS = [
|
|
|
13
15
|
"No project skills found",
|
|
14
16
|
"Try listing global skills",
|
|
15
17
|
];
|
|
16
|
-
function parseStrategy(input) {
|
|
17
|
-
if (!input) {
|
|
18
|
-
return "union";
|
|
19
|
-
}
|
|
20
|
-
if (input === "union" || input === "latest") {
|
|
21
|
-
return input;
|
|
22
|
-
}
|
|
23
|
-
throw new Error(`Invalid strategy "${input}". Use one of: union, latest.`);
|
|
24
|
-
}
|
|
25
18
|
function parseTimestamp(value) {
|
|
26
19
|
if (!value) {
|
|
27
20
|
return null;
|
|
@@ -70,34 +63,35 @@ function areSameSkills(left, right) {
|
|
|
70
63
|
return leftSorted.every((skill, index) => skill.name === rightSorted[index].name &&
|
|
71
64
|
skill.source === rightSorted[index].source);
|
|
72
65
|
}
|
|
73
|
-
function
|
|
66
|
+
function buildMergePlan(params) {
|
|
74
67
|
const localSkills = normalizeSkills(params.localPayload.skills);
|
|
75
68
|
const remoteSkills = normalizeSkills(params.remotePayload.skills);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
:
|
|
82
|
-
|
|
83
|
-
updatedAt: params.nowIso,
|
|
84
|
-
};
|
|
85
|
-
return {
|
|
86
|
-
strategy: "union",
|
|
87
|
-
localSkills,
|
|
88
|
-
remoteSkills,
|
|
89
|
-
installCandidates,
|
|
90
|
-
uploadPayload,
|
|
91
|
-
isRemoteNewer: false,
|
|
69
|
+
const unionSkills = uniqueSortedSkills([...localSkills, ...remoteSkills]);
|
|
70
|
+
const installCandidates = unionSkills.filter((skill) => !localSkills.some((local) => local.name === skill.name && local.source === skill.source));
|
|
71
|
+
const uploadPayload = areSameSkills(remoteSkills, unionSkills)
|
|
72
|
+
? null
|
|
73
|
+
: {
|
|
74
|
+
skills: unionSkills,
|
|
75
|
+
updatedAt: params.nowIso,
|
|
92
76
|
};
|
|
93
|
-
|
|
77
|
+
return {
|
|
78
|
+
mode: "merge",
|
|
79
|
+
localSkills,
|
|
80
|
+
remoteSkills,
|
|
81
|
+
installCandidates,
|
|
82
|
+
uploadPayload,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function buildAutoPlan(params) {
|
|
86
|
+
const localSkills = normalizeSkills(params.localPayload.skills);
|
|
87
|
+
const remoteSkills = normalizeSkills(params.remotePayload.skills);
|
|
94
88
|
const lastSyncTime = parseTimestamp(params.lastSyncAt) ?? 0;
|
|
95
89
|
const remoteTime = parseTimestamp(params.remotePayload.updatedAt);
|
|
96
90
|
const isRemoteNewer = remoteTime !== null && remoteTime > lastSyncTime;
|
|
97
91
|
if (isRemoteNewer) {
|
|
98
92
|
const installCandidates = remoteSkills.filter((skill) => !localSkills.some((local) => local.name === skill.name && local.source === skill.source));
|
|
99
93
|
return {
|
|
100
|
-
|
|
94
|
+
mode: "auto",
|
|
101
95
|
localSkills,
|
|
102
96
|
remoteSkills,
|
|
103
97
|
installCandidates,
|
|
@@ -112,7 +106,7 @@ function buildSyncPlan(params) {
|
|
|
112
106
|
updatedAt: params.nowIso,
|
|
113
107
|
};
|
|
114
108
|
return {
|
|
115
|
-
|
|
109
|
+
mode: "auto",
|
|
116
110
|
localSkills,
|
|
117
111
|
remoteSkills,
|
|
118
112
|
installCandidates: [],
|
|
@@ -120,3 +114,32 @@ function buildSyncPlan(params) {
|
|
|
120
114
|
isRemoteNewer,
|
|
121
115
|
};
|
|
122
116
|
}
|
|
117
|
+
function buildPullPlan(params) {
|
|
118
|
+
const localSkills = normalizeSkills(params.localPayload.skills);
|
|
119
|
+
const remoteSkills = normalizeSkills(params.remotePayload.skills);
|
|
120
|
+
const installCandidates = remoteSkills.filter((remote) => !localSkills.some((local) => local.name === remote.name && local.source === remote.source));
|
|
121
|
+
const removeCandidates = localSkills.filter((local) => !remoteSkills.some((remote) => remote.name === local.name && remote.source === local.source));
|
|
122
|
+
return {
|
|
123
|
+
mode: "pull",
|
|
124
|
+
localSkills,
|
|
125
|
+
remoteSkills,
|
|
126
|
+
installCandidates,
|
|
127
|
+
removeCandidates,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function buildPushPlan(params) {
|
|
131
|
+
const localSkills = normalizeSkills(params.localPayload.skills);
|
|
132
|
+
const remoteSkills = normalizeSkills(params.remotePayload.skills);
|
|
133
|
+
const uploadPayload = areSameSkills(localSkills, remoteSkills)
|
|
134
|
+
? null
|
|
135
|
+
: {
|
|
136
|
+
skills: localSkills,
|
|
137
|
+
updatedAt: params.nowIso,
|
|
138
|
+
};
|
|
139
|
+
return {
|
|
140
|
+
mode: "push",
|
|
141
|
+
localSkills,
|
|
142
|
+
remoteSkills,
|
|
143
|
+
uploadPayload,
|
|
144
|
+
};
|
|
145
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -17,80 +17,100 @@ function getPackageVersion() {
|
|
|
17
17
|
function errorMessage(error) {
|
|
18
18
|
return error instanceof Error ? error.message : String(error);
|
|
19
19
|
}
|
|
20
|
+
function withJsonErrorHandling(action) {
|
|
21
|
+
return async (options) => {
|
|
22
|
+
try {
|
|
23
|
+
await action(options);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (options.json) {
|
|
27
|
+
console.log(JSON.stringify({
|
|
28
|
+
ok: false,
|
|
29
|
+
error: errorMessage(error),
|
|
30
|
+
}, null, 2));
|
|
31
|
+
process.exitCode = 1;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
20
38
|
const program = new commander_1.Command();
|
|
21
39
|
program.name("skillhub").description("SkillHub CLI").version(getPackageVersion());
|
|
22
|
-
program
|
|
40
|
+
const authCommand = program.command("auth").description("Authentication commands");
|
|
41
|
+
authCommand
|
|
23
42
|
.command("login")
|
|
24
|
-
.description("
|
|
43
|
+
.description("Register your GitHub PAT with gist access")
|
|
25
44
|
.action(async () => {
|
|
26
45
|
await (0, login_1.runLogin)();
|
|
27
46
|
});
|
|
28
|
-
|
|
29
|
-
.command("sync")
|
|
30
|
-
.description("Sync: reconcile local skills with remote Gist backup")
|
|
31
|
-
.option("-s, --strategy <strategy>", "merge strategy (union|latest)", "union")
|
|
32
|
-
.option("--dry-run", "show planned changes without applying them", false)
|
|
33
|
-
.option("--json", "print output as JSON", false)
|
|
34
|
-
.action(async (options) => {
|
|
35
|
-
try {
|
|
36
|
-
await (0, sync_1.runSync)({
|
|
37
|
-
strategyInput: options.strategy,
|
|
38
|
-
dryRun: options.dryRun,
|
|
39
|
-
json: options.json,
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
catch (error) {
|
|
43
|
-
if (options.json) {
|
|
44
|
-
console.log(JSON.stringify({
|
|
45
|
-
ok: false,
|
|
46
|
-
error: errorMessage(error),
|
|
47
|
-
}, null, 2));
|
|
48
|
-
process.exitCode = 1;
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
throw error;
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
program
|
|
47
|
+
authCommand
|
|
55
48
|
.command("status")
|
|
56
49
|
.description("Show local auth/sync status")
|
|
57
50
|
.option("--json", "print output as JSON", false)
|
|
58
|
-
.action(async (options) => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
catch (error) {
|
|
63
|
-
if (options.json) {
|
|
64
|
-
console.log(JSON.stringify({
|
|
65
|
-
ok: false,
|
|
66
|
-
error: errorMessage(error),
|
|
67
|
-
}, null, 2));
|
|
68
|
-
process.exitCode = 1;
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
throw error;
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
program
|
|
51
|
+
.action(withJsonErrorHandling(async (options) => {
|
|
52
|
+
await (0, status_1.runStatus)({ json: options.json });
|
|
53
|
+
}));
|
|
54
|
+
authCommand
|
|
75
55
|
.command("logout")
|
|
76
56
|
.description("Clear stored session data (token, gist id, last sync)")
|
|
77
57
|
.option("--yes", "skip confirmation prompt", false)
|
|
78
58
|
.option("--json", "print output as JSON", false)
|
|
79
|
-
.action(async (options) => {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
59
|
+
.action(withJsonErrorHandling(async (options) => {
|
|
60
|
+
await (0, logout_1.runLogout)({ yes: options.yes, json: options.json });
|
|
61
|
+
}));
|
|
62
|
+
const syncCommand = program
|
|
63
|
+
.command("sync")
|
|
64
|
+
.description("Sync local skills and remote Gist backup");
|
|
65
|
+
syncCommand
|
|
66
|
+
.command("pull")
|
|
67
|
+
.description("Mirror remote skills into local skills (remote -> local)")
|
|
68
|
+
.option("--dry-run", "show planned changes without applying them", false)
|
|
69
|
+
.option("--yes", "skip deletion confirmation prompt", false)
|
|
70
|
+
.option("--json", "print output as JSON", false)
|
|
71
|
+
.action(withJsonErrorHandling(async (options) => {
|
|
72
|
+
await (0, sync_1.runSyncPull)({
|
|
73
|
+
dryRun: options.dryRun,
|
|
74
|
+
yes: options.yes,
|
|
75
|
+
json: options.json,
|
|
76
|
+
});
|
|
77
|
+
}));
|
|
78
|
+
syncCommand
|
|
79
|
+
.command("push")
|
|
80
|
+
.description("Mirror local skills into remote backup (local -> remote)")
|
|
81
|
+
.option("--dry-run", "show planned changes without applying them", false)
|
|
82
|
+
.option("--json", "print output as JSON", false)
|
|
83
|
+
.action(withJsonErrorHandling(async (options) => {
|
|
84
|
+
await (0, sync_1.runSyncPush)({
|
|
85
|
+
dryRun: options.dryRun,
|
|
86
|
+
json: options.json,
|
|
87
|
+
});
|
|
88
|
+
}));
|
|
89
|
+
syncCommand
|
|
90
|
+
.command("merge")
|
|
91
|
+
.description("Merge local and remote skills (union behavior)")
|
|
92
|
+
.option("--dry-run", "show planned changes without applying them", false)
|
|
93
|
+
.option("--json", "print output as JSON", false)
|
|
94
|
+
.action(withJsonErrorHandling(async (options) => {
|
|
95
|
+
await (0, sync_1.runSyncMerge)({
|
|
96
|
+
dryRun: options.dryRun,
|
|
97
|
+
json: options.json,
|
|
98
|
+
});
|
|
99
|
+
}));
|
|
100
|
+
syncCommand
|
|
101
|
+
.command("auto")
|
|
102
|
+
.description("Sync using remote.updatedAt and lastSyncAt comparison")
|
|
103
|
+
.option("--dry-run", "show planned changes without applying them", false)
|
|
104
|
+
.option("--json", "print output as JSON", false)
|
|
105
|
+
.action(withJsonErrorHandling(async (options) => {
|
|
106
|
+
await (0, sync_1.runSyncAuto)({
|
|
107
|
+
dryRun: options.dryRun,
|
|
108
|
+
json: options.json,
|
|
109
|
+
});
|
|
110
|
+
}));
|
|
111
|
+
syncCommand.action(() => {
|
|
112
|
+
syncCommand.outputHelp();
|
|
113
|
+
throw new Error("Missing sync mode. Use one of: pull, push, merge, auto.");
|
|
94
114
|
});
|
|
95
115
|
program.parseAsync(process.argv).catch((error) => {
|
|
96
116
|
console.error(`Error: ${errorMessage(error)}`);
|
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.isValidSource = isValidSource;
|
|
7
7
|
exports.getLocalSkills = getLocalSkills;
|
|
8
8
|
exports.installSkills = installSkills;
|
|
9
|
+
exports.removeSkills = removeSkills;
|
|
9
10
|
const node_child_process_1 = require("node:child_process");
|
|
10
11
|
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
11
12
|
const node_os_1 = __importDefault(require("node:os"));
|
|
@@ -14,7 +15,7 @@ const node_util_1 = require("node:util");
|
|
|
14
15
|
const syncCore_1 = require("../core/syncCore");
|
|
15
16
|
const retry_1 = require("../utils/retry");
|
|
16
17
|
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
17
|
-
const
|
|
18
|
+
const SKILLS_LOCK_FILENAMES = ["skills-lock.json", ".skill-lock.json"];
|
|
18
19
|
const DEFAULT_SKILL_SOURCE_REPO = "vercel-labs/agent-skills";
|
|
19
20
|
const NPX_COMMAND = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
20
21
|
const SOURCE_PATTERN = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
@@ -35,26 +36,31 @@ async function runSkillsCommand(args, label) {
|
|
|
35
36
|
function isValidSource(source) {
|
|
36
37
|
return SOURCE_PATTERN.test(source);
|
|
37
38
|
}
|
|
38
|
-
function getCandidateSkillsLockPaths() {
|
|
39
|
-
const cwdPath = node_path_1.default.resolve(process.cwd(),
|
|
40
|
-
const homePath = node_path_1.default.resolve(node_os_1.default.homedir(),
|
|
39
|
+
function getCandidateSkillsLockPaths(filename) {
|
|
40
|
+
const cwdPath = node_path_1.default.resolve(process.cwd(), filename);
|
|
41
|
+
const homePath = node_path_1.default.resolve(node_os_1.default.homedir(), filename);
|
|
41
42
|
const homeConfigPaths = [
|
|
42
|
-
node_path_1.default.resolve(node_os_1.default.homedir(), ".config", "skills",
|
|
43
|
-
node_path_1.default.resolve(node_os_1.default.homedir(), ".config", "skillhub",
|
|
44
|
-
node_path_1.default.resolve(node_os_1.default.homedir(), ".skills",
|
|
43
|
+
node_path_1.default.resolve(node_os_1.default.homedir(), ".config", "skills", filename),
|
|
44
|
+
node_path_1.default.resolve(node_os_1.default.homedir(), ".config", "skillhub", filename),
|
|
45
|
+
node_path_1.default.resolve(node_os_1.default.homedir(), ".skills", filename),
|
|
46
|
+
node_path_1.default.resolve(node_os_1.default.homedir(), ".agents", filename),
|
|
45
47
|
];
|
|
46
48
|
const winAppData = process.env.APPDATA;
|
|
47
49
|
const winLocalAppData = process.env.LOCALAPPDATA;
|
|
48
50
|
const windowsConfigPaths = [
|
|
49
51
|
...(winAppData
|
|
50
|
-
? [node_path_1.default.resolve(winAppData, "skills",
|
|
52
|
+
? [node_path_1.default.resolve(winAppData, "skills", filename)]
|
|
51
53
|
: []),
|
|
52
54
|
...(winLocalAppData
|
|
53
|
-
? [node_path_1.default.resolve(winLocalAppData, "skills",
|
|
55
|
+
? [node_path_1.default.resolve(winLocalAppData, "skills", filename)]
|
|
54
56
|
: []),
|
|
55
57
|
];
|
|
56
58
|
return [cwdPath, homePath, ...homeConfigPaths, ...windowsConfigPaths];
|
|
57
59
|
}
|
|
60
|
+
function getAllCandidateSkillsLockPaths() {
|
|
61
|
+
const paths = SKILLS_LOCK_FILENAMES.flatMap((filename) => getCandidateSkillsLockPaths(filename));
|
|
62
|
+
return [...new Set(paths)];
|
|
63
|
+
}
|
|
58
64
|
function parseSkillsLock(raw) {
|
|
59
65
|
const parsed = JSON.parse(raw);
|
|
60
66
|
const extractSkills = (items) => {
|
|
@@ -82,6 +88,21 @@ function parseSkillsLock(raw) {
|
|
|
82
88
|
if (Array.isArray(parsed?.skills)) {
|
|
83
89
|
return extractSkills(parsed.skills);
|
|
84
90
|
}
|
|
91
|
+
if (parsed?.skills && typeof parsed.skills === "object") {
|
|
92
|
+
const objectSkills = parsed.skills;
|
|
93
|
+
return Object.entries(objectSkills).map(([name, metadata]) => {
|
|
94
|
+
if (typeof metadata === "object" && metadata !== null) {
|
|
95
|
+
const objectItem = metadata;
|
|
96
|
+
const source = typeof objectItem.source === "string"
|
|
97
|
+
? objectItem.source
|
|
98
|
+
: typeof objectItem.repo === "string"
|
|
99
|
+
? objectItem.repo
|
|
100
|
+
: DEFAULT_SKILL_SOURCE_REPO;
|
|
101
|
+
return { name, source };
|
|
102
|
+
}
|
|
103
|
+
return { name, source: DEFAULT_SKILL_SOURCE_REPO };
|
|
104
|
+
});
|
|
105
|
+
}
|
|
85
106
|
if (Array.isArray(parsed)) {
|
|
86
107
|
return extractSkills(parsed);
|
|
87
108
|
}
|
|
@@ -127,6 +148,45 @@ async function tryReadSkillsLock(lockPath) {
|
|
|
127
148
|
return null;
|
|
128
149
|
}
|
|
129
150
|
}
|
|
151
|
+
async function readFirstAvailableSkillsLock() {
|
|
152
|
+
const candidatePaths = getAllCandidateSkillsLockPaths();
|
|
153
|
+
for (const lockPath of candidatePaths) {
|
|
154
|
+
const parsed = await tryReadSkillsLock(lockPath);
|
|
155
|
+
if (parsed && parsed.length > 0) {
|
|
156
|
+
return {
|
|
157
|
+
skills: (0, syncCore_1.normalizeSkills)(parsed),
|
|
158
|
+
candidatePaths,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
skills: [],
|
|
164
|
+
candidatePaths,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function hydrateSourcesFromLock(listSkills, lockSkills) {
|
|
168
|
+
const lockByName = new Map();
|
|
169
|
+
for (const skill of lockSkills) {
|
|
170
|
+
const existing = lockByName.get(skill.name);
|
|
171
|
+
if (existing) {
|
|
172
|
+
existing.push(skill);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
lockByName.set(skill.name, [skill]);
|
|
176
|
+
}
|
|
177
|
+
const hydrated = listSkills.map((skill) => {
|
|
178
|
+
const matches = lockByName.get(skill.name);
|
|
179
|
+
if (!matches || matches.length === 0) {
|
|
180
|
+
return skill;
|
|
181
|
+
}
|
|
182
|
+
const preferred = matches.find((item) => isValidSource(item.source)) ?? matches[0];
|
|
183
|
+
return {
|
|
184
|
+
name: skill.name,
|
|
185
|
+
source: preferred.source,
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
return (0, syncCore_1.normalizeSkills)(hydrated);
|
|
189
|
+
}
|
|
130
190
|
async function getLocalSkills() {
|
|
131
191
|
const listResult = await runSkillsCommand(["list", "-g"], "skills list -g");
|
|
132
192
|
const listOutput = stringifyCommandResult(listResult);
|
|
@@ -135,32 +195,23 @@ async function getLocalSkills() {
|
|
|
135
195
|
return [];
|
|
136
196
|
}
|
|
137
197
|
const fromList = parseSkillsListOutput(listOutput);
|
|
198
|
+
const fromLock = await readFirstAvailableSkillsLock();
|
|
138
199
|
if (fromList.length > 0) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const lockResult = await runSkillsCommand(["generate-lock"], "skills generate-lock");
|
|
142
|
-
const lockOutput = stringifyCommandResult(lockResult);
|
|
143
|
-
const candidatePaths = getCandidateSkillsLockPaths();
|
|
144
|
-
for (const lockPath of candidatePaths) {
|
|
145
|
-
const parsed = await tryReadSkillsLock(lockPath);
|
|
146
|
-
if (parsed) {
|
|
147
|
-
return (0, syncCore_1.normalizeSkills)(parsed);
|
|
200
|
+
if (fromLock.skills.length > 0) {
|
|
201
|
+
return hydrateSourcesFromLock(fromList, fromLock.skills);
|
|
148
202
|
}
|
|
203
|
+
return fromList;
|
|
149
204
|
}
|
|
150
|
-
if (
|
|
151
|
-
|
|
152
|
-
return [];
|
|
205
|
+
if (fromLock.skills.length > 0) {
|
|
206
|
+
return fromLock.skills;
|
|
153
207
|
}
|
|
154
208
|
throw new Error([
|
|
155
209
|
"Unable to construct local skills list.",
|
|
156
210
|
"- skills list -g output:",
|
|
157
211
|
listOutput,
|
|
158
212
|
"",
|
|
159
|
-
|
|
160
|
-
...candidatePaths.map((p) => ` - ${p}`),
|
|
161
|
-
"",
|
|
162
|
-
"- npx skills generate-lock output:",
|
|
163
|
-
lockOutput,
|
|
213
|
+
"- Searched lock paths:",
|
|
214
|
+
...fromLock.candidatePaths.map((p) => ` - ${p}`),
|
|
164
215
|
].join("\n"));
|
|
165
216
|
}
|
|
166
217
|
async function installSkills(skills, options = {}) {
|
|
@@ -195,3 +246,32 @@ async function installSkills(skills, options = {}) {
|
|
|
195
246
|
}
|
|
196
247
|
return { succeeded, failed };
|
|
197
248
|
}
|
|
249
|
+
async function removeSkills(skills, options = {}) {
|
|
250
|
+
const succeeded = [];
|
|
251
|
+
const failed = [];
|
|
252
|
+
const seen = new Set();
|
|
253
|
+
for (const skill of skills) {
|
|
254
|
+
const key = `${skill.source}:${skill.name}`;
|
|
255
|
+
if (seen.has(key)) {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
seen.add(key);
|
|
259
|
+
try {
|
|
260
|
+
const result = await runSkillsCommand(["remove", "--skill", skill.name, "--global", "--yes"], `skills remove --skill ${skill.name}`);
|
|
261
|
+
const output = stringifyCommandResult(result);
|
|
262
|
+
if (options.verbose && output) {
|
|
263
|
+
console.log(output);
|
|
264
|
+
}
|
|
265
|
+
succeeded.push(skill);
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
269
|
+
failed.push({ skill, reason });
|
|
270
|
+
if (options.verbose) {
|
|
271
|
+
console.warn(`Skill remove failed: ${skill.name} (from ${skill.source})`);
|
|
272
|
+
console.warn(` - ${reason}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return { succeeded, failed };
|
|
277
|
+
}
|