@yonpark/skillhub-cli 0.1.2 → 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 -167
- 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/dist/commands/sync.js
CHANGED
|
@@ -1,31 +1,30 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.runSync = runSync;
|
|
7
|
-
const
|
|
8
|
-
const node_util_1 = require("node:util");
|
|
9
|
-
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
10
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
-
const node_os_1 = __importDefault(require("node:os"));
|
|
4
|
+
const syncCore_1 = require("../core/syncCore");
|
|
12
5
|
const config_1 = require("../service/config");
|
|
13
6
|
const gistService_1 = require("../service/gistService");
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
7
|
+
const skillsService_1 = require("../service/skillsService");
|
|
8
|
+
const output_1 = require("../utils/output");
|
|
9
|
+
function formatSyncSummary(summary) {
|
|
10
|
+
const prefix = summary.dryRun ? "Dry-run" : "Sync";
|
|
11
|
+
const failurePart = summary.failed.length > 0
|
|
12
|
+
? ` (${summary.failed.length} failed - check logs or JSON output)`
|
|
13
|
+
: "";
|
|
14
|
+
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)`;
|
|
17
|
+
const details = [
|
|
18
|
+
actionLine + failurePart,
|
|
19
|
+
`strategy=${summary.strategy}`,
|
|
20
|
+
`gistFound=${summary.gistFound}`,
|
|
21
|
+
`gistCreated=${summary.gistCreated}`,
|
|
22
|
+
`remoteNewer=${summary.remoteNewer === null ? "n/a" : String(summary.remoteNewer)}`,
|
|
23
|
+
`lastSyncAtUpdated=${summary.lastSyncAtUpdated}`,
|
|
24
|
+
];
|
|
25
|
+
return details.join("\n");
|
|
26
|
+
}
|
|
27
|
+
async function resolveRemotePayload(token) {
|
|
29
28
|
const octokit = (0, gistService_1.createOctokit)(token);
|
|
30
29
|
let gistId = await config_1.configStore.getGistId();
|
|
31
30
|
let remotePayload = null;
|
|
@@ -38,292 +37,155 @@ async function runSync(strategyInput) {
|
|
|
38
37
|
if (!gistId) {
|
|
39
38
|
const found = await (0, gistService_1.findSkillhubGist)(octokit);
|
|
40
39
|
if (found?.id) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
await
|
|
44
|
-
remotePayload = await safeGetPayload(octokit, foundId);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
if (!gistId) {
|
|
48
|
-
const created = await (0, gistService_1.createSkillhubGist)(octokit, localPayload);
|
|
49
|
-
if (!created.id) {
|
|
50
|
-
throw new Error("Gist was created, but the ID could not be determined.");
|
|
40
|
+
gistId = found.id;
|
|
41
|
+
await config_1.configStore.setGistId(found.id);
|
|
42
|
+
remotePayload = await safeGetPayload(octokit, found.id);
|
|
51
43
|
}
|
|
52
|
-
await config_1.configStore.setGistId(created.id);
|
|
53
|
-
console.log("No existing SkillHub Gist found. A new one has been created.");
|
|
54
|
-
console.log(`Uploaded 1 change, installed 0 skills`);
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
const resolvedRemote = remotePayload
|
|
58
|
-
? {
|
|
59
|
-
...remotePayload,
|
|
60
|
-
skills: normalizeSkills(remotePayload.skills ?? []),
|
|
61
|
-
}
|
|
62
|
-
: { skills: [], updatedAt: "" };
|
|
63
|
-
if (strategy === "latest") {
|
|
64
|
-
await applyLatestStrategy({
|
|
65
|
-
octokit,
|
|
66
|
-
gistId,
|
|
67
|
-
local: localPayload,
|
|
68
|
-
remote: resolvedRemote,
|
|
69
|
-
});
|
|
70
|
-
return;
|
|
71
44
|
}
|
|
72
|
-
|
|
45
|
+
return {
|
|
73
46
|
octokit,
|
|
74
47
|
gistId,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
function parseStrategy(input) {
|
|
80
|
-
if (input === "latest" || input === "union") {
|
|
81
|
-
return input;
|
|
82
|
-
}
|
|
83
|
-
return DEFAULT_STRATEGY;
|
|
84
|
-
}
|
|
85
|
-
async function getLocalSkills() {
|
|
86
|
-
// 1) Primary source: `skills list -g` output
|
|
87
|
-
// - generate-lock can be flaky (e.g. "already in lock file" without writing),
|
|
88
|
-
// so we prefer parsing the list output when possible.
|
|
89
|
-
const listResult = await execAsync("npx skills list -g");
|
|
90
|
-
const listOutput = `${listResult.stdout ?? ""}\n${listResult.stderr ?? ""}`.trim();
|
|
91
|
-
// When there are no global skills, skills CLI prints:
|
|
92
|
-
// "No global skills found.\nTry listing project skills without -g"
|
|
93
|
-
// In that case we treat it as an empty list and avoid polluting Gist.
|
|
94
|
-
if (listOutput.includes("No global skills found") ||
|
|
95
|
-
listOutput.includes("Try listing project skills without -g")) {
|
|
96
|
-
return [];
|
|
97
|
-
}
|
|
98
|
-
const fromList = parseSkillsListOutput(listOutput);
|
|
99
|
-
if (fromList.length > 0) {
|
|
100
|
-
return fromList;
|
|
101
|
-
}
|
|
102
|
-
// 2) Fallback: generate-lock + search for a skills-lock.json file
|
|
103
|
-
const { stdout, stderr } = await execAsync("npx skills generate-lock");
|
|
104
|
-
const output = `${stdout ?? ""}\n${stderr ?? ""}`.trim();
|
|
105
|
-
const candidatePaths = getCandidateSkillsLockPaths();
|
|
106
|
-
for (const lockPath of candidatePaths) {
|
|
107
|
-
const parsed = await tryReadSkillsLock(lockPath);
|
|
108
|
-
if (parsed) {
|
|
109
|
-
return parsed;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
if (output.includes("No installed skills found") || listOutput.includes("No project skills found")) {
|
|
113
|
-
return [];
|
|
114
|
-
}
|
|
115
|
-
throw new Error([
|
|
116
|
-
`Unable to construct local skills list.`,
|
|
117
|
-
`- skills list -g output:`,
|
|
118
|
-
listOutput,
|
|
119
|
-
``,
|
|
120
|
-
`- Searched ${SKILLS_LOCK_FILENAME} paths:`,
|
|
121
|
-
...candidatePaths.map((p) => ` - ${p}`),
|
|
122
|
-
``,
|
|
123
|
-
`- npx skills generate-lock output:`,
|
|
124
|
-
output,
|
|
125
|
-
].join("\n"));
|
|
126
|
-
}
|
|
127
|
-
function getCandidateSkillsLockPaths() {
|
|
128
|
-
const cwdPath = node_path_1.default.resolve(process.cwd(), SKILLS_LOCK_FILENAME);
|
|
129
|
-
const homePath = node_path_1.default.resolve(node_os_1.default.homedir(), SKILLS_LOCK_FILENAME);
|
|
130
|
-
// Cross-platform candidates when we don't know exactly where the tool writes
|
|
131
|
-
const homeConfigPaths = [
|
|
132
|
-
node_path_1.default.resolve(node_os_1.default.homedir(), ".config", "skills", SKILLS_LOCK_FILENAME),
|
|
133
|
-
node_path_1.default.resolve(node_os_1.default.homedir(), ".config", "skillhub", SKILLS_LOCK_FILENAME),
|
|
134
|
-
node_path_1.default.resolve(node_os_1.default.homedir(), ".skills", SKILLS_LOCK_FILENAME),
|
|
135
|
-
];
|
|
136
|
-
// Windows-specific candidates
|
|
137
|
-
const winAppData = process.env.APPDATA;
|
|
138
|
-
const winLocalAppData = process.env.LOCALAPPDATA;
|
|
139
|
-
const windowsConfigPaths = [
|
|
140
|
-
...(winAppData
|
|
141
|
-
? [node_path_1.default.resolve(winAppData, "skills", SKILLS_LOCK_FILENAME)]
|
|
142
|
-
: []),
|
|
143
|
-
...(winLocalAppData
|
|
144
|
-
? [node_path_1.default.resolve(winLocalAppData, "skills", SKILLS_LOCK_FILENAME)]
|
|
145
|
-
: []),
|
|
146
|
-
];
|
|
147
|
-
return [cwdPath, homePath, ...homeConfigPaths, ...windowsConfigPaths];
|
|
48
|
+
remotePayload: remotePayload ?? { skills: [], updatedAt: "" },
|
|
49
|
+
};
|
|
148
50
|
}
|
|
149
|
-
async function
|
|
51
|
+
async function safeGetPayload(octokit, gistId) {
|
|
150
52
|
try {
|
|
151
|
-
|
|
152
|
-
return parseSkillsLock(raw);
|
|
53
|
+
return await (0, gistService_1.getSkillhubPayload)(octokit, gistId);
|
|
153
54
|
}
|
|
154
55
|
catch {
|
|
155
56
|
return null;
|
|
156
57
|
}
|
|
157
58
|
}
|
|
158
|
-
function
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return { name: String(item), source: DEFAULT_SKILL_SOURCE_REPO };
|
|
172
|
-
});
|
|
59
|
+
function createSummaryFromPlan(params) {
|
|
60
|
+
return {
|
|
61
|
+
ok: params.failed.length === 0,
|
|
62
|
+
strategy: params.strategy,
|
|
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,
|
|
173
72
|
};
|
|
174
|
-
const fromSkills = parsed?.skills;
|
|
175
|
-
if (Array.isArray(fromSkills)) {
|
|
176
|
-
return extractSkills(fromSkills);
|
|
177
|
-
}
|
|
178
|
-
if (Array.isArray(parsed)) {
|
|
179
|
-
return extractSkills(parsed);
|
|
180
|
-
}
|
|
181
|
-
const fromInstalled = parsed?.installedSkills;
|
|
182
|
-
if (Array.isArray(fromInstalled)) {
|
|
183
|
-
return extractSkills(fromInstalled);
|
|
184
|
-
}
|
|
185
|
-
return [];
|
|
186
73
|
}
|
|
187
|
-
function
|
|
188
|
-
const
|
|
189
|
-
const
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (line === "Global Skills")
|
|
195
|
-
continue;
|
|
196
|
-
if (line.startsWith("Agents:"))
|
|
197
|
-
continue;
|
|
198
|
-
if (line.startsWith("No project skills found"))
|
|
199
|
-
continue;
|
|
200
|
-
if (line.startsWith("Try listing global skills"))
|
|
201
|
-
continue;
|
|
202
|
-
if (line.startsWith("No global skills found"))
|
|
203
|
-
continue;
|
|
204
|
-
if (line.startsWith("Try listing project skills without -g"))
|
|
205
|
-
continue;
|
|
206
|
-
// Example: "find-skills ~\\.agents\\skills\\find-skills"
|
|
207
|
-
// We only take the first token as the skill name
|
|
208
|
-
const [name] = line.split(/\s+/);
|
|
209
|
-
if (!name)
|
|
210
|
-
continue;
|
|
211
|
-
// Skip obvious path-like tokens as a safety net
|
|
212
|
-
if (name.includes("\\") || name.includes("/") || name.includes("~"))
|
|
213
|
-
continue;
|
|
214
|
-
// skills list 출력에는 source 정보가 없으므로 기본값 사용
|
|
215
|
-
skills.push({ name, source: DEFAULT_SKILL_SOURCE_REPO });
|
|
74
|
+
async function runSync(options = {}) {
|
|
75
|
+
const strategy = (0, syncCore_1.parseStrategy)(options.strategyInput);
|
|
76
|
+
const dryRun = options.dryRun === true;
|
|
77
|
+
const asJson = options.json === true;
|
|
78
|
+
const token = await config_1.configStore.getToken();
|
|
79
|
+
if (!token) {
|
|
80
|
+
throw new Error("You must login first. Run `skillhub login` and try again.");
|
|
216
81
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
82
|
+
const nowIso = new Date().toISOString();
|
|
83
|
+
const localSkills = await (0, skillsService_1.getLocalSkills)();
|
|
84
|
+
const localPayload = {
|
|
85
|
+
skills: localSkills,
|
|
86
|
+
updatedAt: nowIso,
|
|
87
|
+
};
|
|
88
|
+
const { octokit, gistId, remotePayload } = await resolveRemotePayload(token);
|
|
89
|
+
const hasRemoteGist = Boolean(gistId);
|
|
90
|
+
if (!hasRemoteGist) {
|
|
91
|
+
if (dryRun) {
|
|
92
|
+
const summary = createSummaryFromPlan({
|
|
93
|
+
strategy,
|
|
94
|
+
dryRun: true,
|
|
95
|
+
gistFound: false,
|
|
96
|
+
gistCreated: false,
|
|
97
|
+
remoteNewer: null,
|
|
98
|
+
uploaded: 1,
|
|
99
|
+
installPlanned: 0,
|
|
100
|
+
installed: 0,
|
|
101
|
+
failed: [],
|
|
102
|
+
lastSyncAtUpdated: false,
|
|
103
|
+
});
|
|
104
|
+
(0, output_1.emitOutput)(summary, asJson, formatSyncSummary);
|
|
105
|
+
return summary;
|
|
230
106
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
!bannedSubstrings.some((bad) => skill.name.includes(bad)));
|
|
235
|
-
return uniqueSortedSkills(normalized);
|
|
236
|
-
}
|
|
237
|
-
async function safeGetPayload(octokit, gistId) {
|
|
238
|
-
try {
|
|
239
|
-
return await (0, gistService_1.getSkillhubPayload)(octokit, gistId);
|
|
240
|
-
}
|
|
241
|
-
catch {
|
|
242
|
-
return null;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
function uniqueSortedSkills(skills) {
|
|
246
|
-
const seen = new Set();
|
|
247
|
-
const result = [];
|
|
248
|
-
for (const skill of skills) {
|
|
249
|
-
const key = `${skill.source}:${skill.name}`;
|
|
250
|
-
if (!seen.has(key)) {
|
|
251
|
-
seen.add(key);
|
|
252
|
-
result.push(skill);
|
|
107
|
+
const created = await (0, gistService_1.createSkillhubGist)(octokit, localPayload);
|
|
108
|
+
if (!created.id) {
|
|
109
|
+
throw new Error("Gist was created, but the ID could not be determined.");
|
|
253
110
|
}
|
|
111
|
+
await config_1.configStore.setGistId(created.id);
|
|
112
|
+
await config_1.configStore.setLastSyncAt(nowIso);
|
|
113
|
+
const summary = createSummaryFromPlan({
|
|
114
|
+
strategy,
|
|
115
|
+
dryRun: false,
|
|
116
|
+
gistFound: false,
|
|
117
|
+
gistCreated: true,
|
|
118
|
+
remoteNewer: null,
|
|
119
|
+
uploaded: 1,
|
|
120
|
+
installPlanned: 0,
|
|
121
|
+
installed: 0,
|
|
122
|
+
failed: [],
|
|
123
|
+
lastSyncAtUpdated: true,
|
|
124
|
+
});
|
|
125
|
+
(0, output_1.emitOutput)(summary, asJson, formatSyncSummary);
|
|
126
|
+
return summary;
|
|
127
|
+
}
|
|
128
|
+
const lastSyncAt = await config_1.configStore.getLastSyncAt();
|
|
129
|
+
const plan = (0, syncCore_1.buildSyncPlan)({
|
|
130
|
+
strategy,
|
|
131
|
+
localPayload,
|
|
132
|
+
remotePayload,
|
|
133
|
+
lastSyncAt,
|
|
134
|
+
nowIso,
|
|
135
|
+
});
|
|
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));
|
|
143
|
+
if (dryRun) {
|
|
144
|
+
const summary = createSummaryFromPlan({
|
|
145
|
+
strategy,
|
|
146
|
+
dryRun: true,
|
|
147
|
+
gistFound: true,
|
|
148
|
+
gistCreated: false,
|
|
149
|
+
remoteNewer: strategy === "latest" ? plan.isRemoteNewer : null,
|
|
150
|
+
uploaded: plan.uploadPayload ? 1 : 0,
|
|
151
|
+
installPlanned: plan.installCandidates.length,
|
|
152
|
+
installed: 0,
|
|
153
|
+
failed: invalidInstallCandidates,
|
|
154
|
+
lastSyncAtUpdated: false,
|
|
155
|
+
});
|
|
156
|
+
(0, output_1.emitOutput)(summary, asJson, formatSyncSummary);
|
|
157
|
+
return summary;
|
|
254
158
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
return a.source.localeCompare(b.source);
|
|
258
|
-
}
|
|
259
|
-
return a.name.localeCompare(b.name);
|
|
159
|
+
const installResult = await (0, skillsService_1.installSkills)(validInstallCandidates, {
|
|
160
|
+
verbose: !asJson,
|
|
260
161
|
});
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
162
|
+
const failed = [...invalidInstallCandidates, ...installResult.failed];
|
|
163
|
+
if (plan.uploadPayload) {
|
|
164
|
+
await (0, gistService_1.updateSkillhubGist)(octokit, gistId, plan.uploadPayload);
|
|
165
|
+
}
|
|
166
|
+
const summary = createSummaryFromPlan({
|
|
167
|
+
strategy,
|
|
168
|
+
dryRun: false,
|
|
169
|
+
gistFound: true,
|
|
170
|
+
gistCreated: false,
|
|
171
|
+
remoteNewer: strategy === "latest" ? plan.isRemoteNewer : null,
|
|
172
|
+
uploaded: plan.uploadPayload ? 1 : 0,
|
|
173
|
+
installPlanned: plan.installCandidates.length,
|
|
174
|
+
installed: installResult.succeeded.length,
|
|
175
|
+
failed,
|
|
176
|
+
lastSyncAtUpdated: false,
|
|
177
|
+
});
|
|
178
|
+
if (failed.length === 0) {
|
|
179
|
+
await config_1.configStore.setLastSyncAt(nowIso);
|
|
180
|
+
summary.lastSyncAtUpdated = true;
|
|
181
|
+
}
|
|
182
|
+
(0, output_1.emitOutput)(summary, asJson, formatSyncSummary);
|
|
183
|
+
if (failed.length > 0) {
|
|
184
|
+
if (asJson) {
|
|
185
|
+
process.exitCode = 1;
|
|
186
|
+
return summary;
|
|
285
187
|
}
|
|
188
|
+
throw new Error(`Sync completed with ${failed.length} failed install(s). Check logs above.`);
|
|
286
189
|
}
|
|
287
|
-
return
|
|
288
|
-
}
|
|
289
|
-
async function applyUnionStrategy(params) {
|
|
290
|
-
const localSkills = normalizeSkills(params.local.skills);
|
|
291
|
-
const remoteSkills = normalizeSkills(params.remote.skills);
|
|
292
|
-
const unionSkills = uniqueSortedSkills([...localSkills, ...remoteSkills]);
|
|
293
|
-
const missingLocally = unionSkills.filter((skill) => !localSkills.some((local) => local.name === skill.name && local.source === skill.source));
|
|
294
|
-
const payload = {
|
|
295
|
-
skills: unionSkills,
|
|
296
|
-
updatedAt: new Date().toISOString(),
|
|
297
|
-
};
|
|
298
|
-
const { succeeded, failed } = await installSkills(missingLocally);
|
|
299
|
-
await (0, gistService_1.updateSkillhubGist)(params.octokit, params.gistId, payload);
|
|
300
|
-
const uploadCount = areSameSkills(remoteSkills, unionSkills) ? 0 : 1;
|
|
301
|
-
const installCount = succeeded.length;
|
|
302
|
-
const failedCount = failed.length;
|
|
303
|
-
console.log(`업로드 ${uploadCount}건, 설치 ${installCount}건${failedCount ? ` (실패 ${failedCount}건은 로그 참고)` : ""}`);
|
|
304
|
-
}
|
|
305
|
-
async function applyLatestStrategy(params) {
|
|
306
|
-
const localTime = Date.parse(params.local.updatedAt);
|
|
307
|
-
const remoteTime = Date.parse(params.remote.updatedAt);
|
|
308
|
-
const isRemoteNewer = Number.isFinite(remoteTime) && remoteTime > localTime;
|
|
309
|
-
if (isRemoteNewer) {
|
|
310
|
-
const localSkills = normalizeSkills(params.local.skills);
|
|
311
|
-
const remoteSkills = normalizeSkills(params.remote.skills);
|
|
312
|
-
const missingLocally = remoteSkills.filter((skill) => !localSkills.some((local) => local.name === skill.name && local.source === skill.source));
|
|
313
|
-
const { succeeded, failed } = await installSkills(missingLocally);
|
|
314
|
-
const failedCount = failed.length;
|
|
315
|
-
console.log(`업로드 0건, 설치 ${succeeded.length}건${failedCount ? ` (실패 ${failedCount}건은 로그 참고)` : ""}`);
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
await (0, gistService_1.updateSkillhubGist)(params.octokit, params.gistId, params.local);
|
|
319
|
-
console.log("업로드 1건, 설치 0건");
|
|
320
|
-
}
|
|
321
|
-
function areSameSkills(left, right) {
|
|
322
|
-
const leftSorted = uniqueSortedSkills(left);
|
|
323
|
-
const rightSorted = uniqueSortedSkills(right);
|
|
324
|
-
if (leftSorted.length !== rightSorted.length) {
|
|
325
|
-
return false;
|
|
326
|
-
}
|
|
327
|
-
return leftSorted.every((skill, index) => skill.name === rightSorted[index].name &&
|
|
328
|
-
skill.source === rightSorted[index].source);
|
|
190
|
+
return summary;
|
|
329
191
|
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseStrategy = parseStrategy;
|
|
4
|
+
exports.parseTimestamp = parseTimestamp;
|
|
5
|
+
exports.normalizeSkills = normalizeSkills;
|
|
6
|
+
exports.uniqueSortedSkills = uniqueSortedSkills;
|
|
7
|
+
exports.areSameSkills = areSameSkills;
|
|
8
|
+
exports.buildSyncPlan = buildSyncPlan;
|
|
9
|
+
const DEFAULT_SKILL_SOURCE_REPO = "vercel-labs/agent-skills";
|
|
10
|
+
const BANNED_SKILL_NAME_SUBSTRINGS = [
|
|
11
|
+
"No global skills found",
|
|
12
|
+
"Try listing project skills without -g",
|
|
13
|
+
"No project skills found",
|
|
14
|
+
"Try listing global skills",
|
|
15
|
+
];
|
|
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
|
+
function parseTimestamp(value) {
|
|
26
|
+
if (!value) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const parsed = Date.parse(value);
|
|
30
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
31
|
+
}
|
|
32
|
+
function normalizeSkills(skills) {
|
|
33
|
+
const normalized = skills
|
|
34
|
+
.map((skill) => {
|
|
35
|
+
if (typeof skill === "string") {
|
|
36
|
+
return { name: skill, source: DEFAULT_SKILL_SOURCE_REPO };
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
name: String(skill?.name ?? ""),
|
|
40
|
+
source: String(skill?.source ?? DEFAULT_SKILL_SOURCE_REPO),
|
|
41
|
+
};
|
|
42
|
+
})
|
|
43
|
+
.filter((skill) => skill.name.length > 0 &&
|
|
44
|
+
!BANNED_SKILL_NAME_SUBSTRINGS.some((bad) => skill.name.includes(bad)));
|
|
45
|
+
return uniqueSortedSkills(normalized);
|
|
46
|
+
}
|
|
47
|
+
function uniqueSortedSkills(skills) {
|
|
48
|
+
const seen = new Set();
|
|
49
|
+
const result = [];
|
|
50
|
+
for (const skill of skills) {
|
|
51
|
+
const key = `${skill.source}:${skill.name}`;
|
|
52
|
+
if (!seen.has(key)) {
|
|
53
|
+
seen.add(key);
|
|
54
|
+
result.push(skill);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return result.sort((a, b) => {
|
|
58
|
+
if (a.source !== b.source) {
|
|
59
|
+
return a.source.localeCompare(b.source);
|
|
60
|
+
}
|
|
61
|
+
return a.name.localeCompare(b.name);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function areSameSkills(left, right) {
|
|
65
|
+
const leftSorted = uniqueSortedSkills(left);
|
|
66
|
+
const rightSorted = uniqueSortedSkills(right);
|
|
67
|
+
if (leftSorted.length !== rightSorted.length) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
return leftSorted.every((skill, index) => skill.name === rightSorted[index].name &&
|
|
71
|
+
skill.source === rightSorted[index].source);
|
|
72
|
+
}
|
|
73
|
+
function buildSyncPlan(params) {
|
|
74
|
+
const localSkills = normalizeSkills(params.localPayload.skills);
|
|
75
|
+
const remoteSkills = normalizeSkills(params.remotePayload.skills);
|
|
76
|
+
if (params.strategy === "union") {
|
|
77
|
+
const unionSkills = uniqueSortedSkills([...localSkills, ...remoteSkills]);
|
|
78
|
+
const installCandidates = unionSkills.filter((skill) => !localSkills.some((local) => local.name === skill.name && local.source === skill.source));
|
|
79
|
+
const uploadPayload = areSameSkills(remoteSkills, unionSkills)
|
|
80
|
+
? null
|
|
81
|
+
: {
|
|
82
|
+
skills: unionSkills,
|
|
83
|
+
updatedAt: params.nowIso,
|
|
84
|
+
};
|
|
85
|
+
return {
|
|
86
|
+
strategy: "union",
|
|
87
|
+
localSkills,
|
|
88
|
+
remoteSkills,
|
|
89
|
+
installCandidates,
|
|
90
|
+
uploadPayload,
|
|
91
|
+
isRemoteNewer: false,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const lastSyncTime = parseTimestamp(params.lastSyncAt) ?? 0;
|
|
95
|
+
const remoteTime = parseTimestamp(params.remotePayload.updatedAt);
|
|
96
|
+
const isRemoteNewer = remoteTime !== null && remoteTime > lastSyncTime;
|
|
97
|
+
if (isRemoteNewer) {
|
|
98
|
+
const installCandidates = remoteSkills.filter((skill) => !localSkills.some((local) => local.name === skill.name && local.source === skill.source));
|
|
99
|
+
return {
|
|
100
|
+
strategy: "latest",
|
|
101
|
+
localSkills,
|
|
102
|
+
remoteSkills,
|
|
103
|
+
installCandidates,
|
|
104
|
+
uploadPayload: null,
|
|
105
|
+
isRemoteNewer,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const uploadPayload = areSameSkills(localSkills, remoteSkills)
|
|
109
|
+
? null
|
|
110
|
+
: {
|
|
111
|
+
skills: localSkills,
|
|
112
|
+
updatedAt: params.nowIso,
|
|
113
|
+
};
|
|
114
|
+
return {
|
|
115
|
+
strategy: "latest",
|
|
116
|
+
localSkills,
|
|
117
|
+
remoteSkills,
|
|
118
|
+
installCandidates: [],
|
|
119
|
+
uploadPayload,
|
|
120
|
+
isRemoteNewer,
|
|
121
|
+
};
|
|
122
|
+
}
|