@yonpark/skillhub-cli 0.2.0 → 0.3.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 +48 -33
- 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 +30 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,55 +24,69 @@ 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
|
|
53
|
+
Common options:
|
|
51
54
|
|
|
52
|
-
|
|
55
|
+
```bash
|
|
56
|
+
--dry-run # compute plan only (no install/remove/upload/config write)
|
|
57
|
+
--json # single JSON output object
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`pull` only:
|
|
53
61
|
|
|
54
62
|
```bash
|
|
55
|
-
|
|
56
|
-
skillhub status --json
|
|
63
|
+
--yes # skip deletion confirmation prompt
|
|
57
64
|
```
|
|
58
65
|
|
|
59
|
-
|
|
66
|
+
Mode behavior:
|
|
60
67
|
|
|
61
|
-
-
|
|
62
|
-
-
|
|
63
|
-
-
|
|
64
|
-
- local
|
|
65
|
-
- remote Gist API accessibility
|
|
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.
|
|
66
72
|
|
|
67
|
-
|
|
73
|
+
## Migration (Breaking Changes in 0.3.0)
|
|
68
74
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
skillhub
|
|
72
|
-
skillhub
|
|
73
|
-
|
|
75
|
+
Removed commands:
|
|
76
|
+
|
|
77
|
+
- `skillhub login`
|
|
78
|
+
- `skillhub status`
|
|
79
|
+
- `skillhub logout`
|
|
80
|
+
- `skillhub sync --strategy ...`
|
|
81
|
+
- `skillhub sync` (without a subcommand)
|
|
74
82
|
|
|
75
|
-
|
|
83
|
+
New mapping:
|
|
84
|
+
|
|
85
|
+
- `skillhub login` -> `skillhub auth login`
|
|
86
|
+
- `skillhub status` -> `skillhub auth status`
|
|
87
|
+
- `skillhub logout` -> `skillhub auth logout`
|
|
88
|
+
- `skillhub sync --strategy union` -> `skillhub sync merge`
|
|
89
|
+
- `skillhub sync --strategy latest` -> `skillhub sync auto`
|
|
76
90
|
|
|
77
91
|
## Payload Format
|
|
78
92
|
|
|
@@ -87,7 +101,7 @@ Clears stored session keys: `githubToken`, `gistId`, `lastSyncAt`.
|
|
|
87
101
|
}
|
|
88
102
|
```
|
|
89
103
|
|
|
90
|
-
Backward compatibility for legacy `skills: string[]` is preserved.
|
|
104
|
+
Backward compatibility for legacy `skills: string[]` payloads is preserved.
|
|
91
105
|
|
|
92
106
|
## Local npm Credentials
|
|
93
107
|
|
|
@@ -113,9 +127,10 @@ Publish:
|
|
|
113
127
|
npm run release
|
|
114
128
|
```
|
|
115
129
|
|
|
116
|
-
CI release workflow
|
|
130
|
+
CI release workflow publishes to GitHub Packages only:
|
|
117
131
|
|
|
118
|
-
- `NPM_TOKEN` repository secret for npmjs publish of `@yonpark/skillhub-cli`.
|
|
119
132
|
- GitHub Packages publish uses `GITHUB_TOKEN` by default.
|
|
120
133
|
- If needed, add `GH_PACKAGES_TOKEN` (PAT with `write:packages`) for GitHub Packages.
|
|
121
134
|
- GitHub Packages target package is `@yw9142/skillhub-cli`.
|
|
135
|
+
|
|
136
|
+
npmjs publish is manual.
|
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"));
|
|
@@ -195,3 +196,32 @@ async function installSkills(skills, options = {}) {
|
|
|
195
196
|
}
|
|
196
197
|
return { succeeded, failed };
|
|
197
198
|
}
|
|
199
|
+
async function removeSkills(skills, options = {}) {
|
|
200
|
+
const succeeded = [];
|
|
201
|
+
const failed = [];
|
|
202
|
+
const seen = new Set();
|
|
203
|
+
for (const skill of skills) {
|
|
204
|
+
const key = `${skill.source}:${skill.name}`;
|
|
205
|
+
if (seen.has(key)) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
seen.add(key);
|
|
209
|
+
try {
|
|
210
|
+
const result = await runSkillsCommand(["remove", "--skill", skill.name, "--global", "--yes"], `skills remove --skill ${skill.name}`);
|
|
211
|
+
const output = stringifyCommandResult(result);
|
|
212
|
+
if (options.verbose && output) {
|
|
213
|
+
console.log(output);
|
|
214
|
+
}
|
|
215
|
+
succeeded.push(skill);
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
219
|
+
failed.push({ skill, reason });
|
|
220
|
+
if (options.verbose) {
|
|
221
|
+
console.warn(`Skill remove failed: ${skill.name} (from ${skill.source})`);
|
|
222
|
+
console.warn(` - ${reason}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return { succeeded, failed };
|
|
227
|
+
}
|