@yonpark/skillhub-cli 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yonghun Park
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,167 @@
1
+ # SkillHub CLI (Gist Edition)
2
+
3
+ SkillHub is a small CLI that syncs your local AI agent skills (managed by `npx skills`)
4
+ with a single GitHub Gist. The Gist acts as a free, serverless backup so you can
5
+ recreate your skill setup on any machine.
6
+
7
+ ## Features
8
+
9
+ - **Login** with a GitHub Personal Access Token (classic) that has `gist` scope
10
+ - **Sync** local skills with a remote Gist:
11
+ - Push new local skills up to Gist
12
+ - Pull missing skills down from Gist and install them globally
13
+ - Simple merge strategies (`union` and `latest`)
14
+
15
+ ## Installation
16
+
17
+ From the project root:
18
+
19
+ ```bash
20
+ npm install
21
+ npm run build
22
+ npm link # optional, to get a global `skillhub` command
23
+ ```
24
+
25
+ Alternatively, you can use `npx`:
26
+
27
+ ```bash
28
+ npm run build
29
+ npx skillhub-cli <command>
30
+ ```
31
+
32
+ ## Commands
33
+
34
+ ### `skillhub login`
35
+
36
+ Registers a GitHub token locally so the CLI can talk to the Gist API.
37
+
38
+ The command will:
39
+
40
+ 1. Prompt for a token:
41
+ - Create a **Personal Access Token (classic)** in GitHub
42
+ - Grant at least the **`gist`** scope
43
+ 2. Verify that the token is valid and can access Gists
44
+ 3. Store the token locally using [`conf`](https://www.npmjs.com/package/conf)
45
+
46
+ If login fails, the CLI explains that the token is invalid or missing gist
47
+ permissions, and you can paste a new one.
48
+
49
+ ### `skillhub sync`
50
+
51
+ Synchronizes your local skills with the remote Gist.
52
+
53
+ ```bash
54
+ skillhub sync
55
+ skillhub sync --strategy union
56
+ skillhub sync --strategy latest
57
+ ```
58
+
59
+ #### What “sync” actually does
60
+
61
+ 1. **Read local skills**
62
+ - Runs `npx skills list -g` and parses the global skills:
63
+ - For example:
64
+ ```text
65
+ Global Skills
66
+
67
+ vercel-composition-patterns ~\.agents\skills\vercel-composition-patterns
68
+ vercel-react-best-practices ~\.agents\skills\vercel-react-best-practices
69
+ ```
70
+ - Falls back to `npx skills generate-lock` + searching for a
71
+ `skills-lock.json` file when necessary.
72
+
73
+ 2. **Read remote skills (from Gist)**
74
+ - Looks for a private Gist that contains `skillhub.json`
75
+ - If not found, creates a new Gist
76
+
77
+ 3. **Merge local vs remote**
78
+ - **`union` (default)**:
79
+ - Takes the set union of local and remote skill names
80
+ - Any skills only on Gist are installed locally
81
+ - Any skills only on local are written to Gist
82
+ - **`latest`**:
83
+ - Compares `updatedAt` timestamps in the payloads
84
+ - If remote is newer:
85
+ - Installs skills that are missing locally
86
+ - Gist is treated as the source of truth
87
+ - If local is newer (or timestamps are invalid):
88
+ - Overwrites the Gist with local data
89
+
90
+ 4. **Apply changes**
91
+ - For skills that exist remotely but not locally, SkillHub runs:
92
+ ```bash
93
+ npx skills add vercel-labs/agent-skills --skill "<skill-name>" -g -y
94
+ ```
95
+ - For skills that exist locally but not in the Gist, the CLI updates
96
+ `skillhub.json` in the Gist to reflect the union
97
+
98
+ 5. **Summary output**
99
+
100
+ After a run you’ll see a short summary like:
101
+
102
+ ```text
103
+ Uploaded 1 change, installed 0 skills
104
+ Uploaded 0 changes, installed 4 skills (1 install failed – check logs)
105
+ ```
106
+
107
+ ## Gist Payload
108
+
109
+ The Gist file `skillhub.json` currently uses a simple payload:
110
+
111
+ ```json
112
+ {
113
+ "skills": ["vercel-composition-patterns", "vercel-react-best-practices"],
114
+ "updatedAt": "2026-01-29T07:27:53.844Z"
115
+ }
116
+ ```
117
+
118
+ - `skills`: list of skill names as reported by `skills list -g`
119
+ - `updatedAt`: ISO timestamp when the payload was last written
120
+
121
+ This keeps the format easy to inspect and edit directly in GitHub if needed.
122
+
123
+ ## Typical Workflows
124
+
125
+ ### First machine (create backup)
126
+
127
+ ```bash
128
+ # 1) Install skills using the official CLI
129
+ npx skills add vercel-labs/agent-skills --all -g -y
130
+
131
+ # 2) Login once
132
+ skillhub login
133
+
134
+ # 3) Push your current skills to Gist
135
+ skillhub sync
136
+ ```
137
+
138
+ ### New machine (restore from backup)
139
+
140
+ ```bash
141
+ # 1) Install and build SkillHub CLI
142
+ npm install
143
+ npm run build
144
+ npm link
145
+
146
+ # 2) Login with the same GitHub account/token
147
+ skillhub login
148
+
149
+ # 3) Pull skills from Gist and install missing ones
150
+ skillhub sync
151
+ ```
152
+
153
+ ## Notes and Limitations
154
+
155
+ - The installer assumes your skills come from
156
+ `vercel-labs/agent-skills` and calls:
157
+ ```bash
158
+ npx skills add vercel-labs/agent-skills --skill "<skill-name>" -g -y
159
+ ```
160
+ If a skill name doesn’t exist in that repo, the install will fail but
161
+ the overall sync will continue.
162
+ - The CLI currently focuses on **global** skills (`skills list -g`),
163
+ not project-scoped skills.
164
+ - Error messages try to surface both:
165
+ - Which step failed (local list / lock file / install / Gist)
166
+ - The underlying CLI output for easier debugging
167
+
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+
3
+ const path = require("path");
4
+
5
+ const entryPath = path.resolve(__dirname, "..", "dist", "index.js");
6
+ require(entryPath);
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runLogin = runLogin;
4
+ const config_1 = require("../service/config");
5
+ const gistService_1 = require("../service/gistService");
6
+ const TOKEN_PROMPT_MESSAGE = "Create a GitHub Personal Access Token (classic) with the `gist` scope, then paste it here:";
7
+ async function runLogin() {
8
+ const { default: inquirer } = await import("inquirer");
9
+ const { token } = await inquirer.prompt([
10
+ {
11
+ type: "password",
12
+ name: "token",
13
+ message: TOKEN_PROMPT_MESSAGE,
14
+ mask: "*",
15
+ validate: (value) => value.trim().length > 0 ? true : "Please enter a token.",
16
+ },
17
+ ]);
18
+ const trimmedToken = token.trim();
19
+ try {
20
+ await (0, gistService_1.verifyToken)(trimmedToken);
21
+ await config_1.configStore.setToken(trimmedToken);
22
+ console.log("Login successful: token has been saved.");
23
+ }
24
+ catch (error) {
25
+ console.error([
26
+ "Login failed: token is invalid or cannot access the Gist API.",
27
+ "Please create a GitHub Personal Access Token (classic) with the `gist` scope and try again.",
28
+ ].join("\n"));
29
+ throw error;
30
+ }
31
+ }
@@ -0,0 +1,329 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runSync = runSync;
7
+ const node_child_process_1 = require("node:child_process");
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"));
12
+ const config_1 = require("../service/config");
13
+ const gistService_1 = require("../service/gistService");
14
+ const execAsync = (0, node_util_1.promisify)(node_child_process_1.exec);
15
+ const SKILLS_LOCK_FILENAME = "skills-lock.json";
16
+ const DEFAULT_STRATEGY = "union";
17
+ const DEFAULT_SKILL_SOURCE_REPO = "vercel-labs/agent-skills";
18
+ async function runSync(strategyInput) {
19
+ const strategy = parseStrategy(strategyInput);
20
+ const token = await config_1.configStore.getToken();
21
+ if (!token) {
22
+ throw new Error("You must login first. Run `skillhub login` and try again.");
23
+ }
24
+ const localSkills = await getLocalSkills();
25
+ const localPayload = {
26
+ skills: uniqueSortedSkills(localSkills),
27
+ updatedAt: new Date().toISOString(),
28
+ };
29
+ const octokit = (0, gistService_1.createOctokit)(token);
30
+ let gistId = await config_1.configStore.getGistId();
31
+ let remotePayload = null;
32
+ if (gistId) {
33
+ remotePayload = await safeGetPayload(octokit, gistId);
34
+ if (!remotePayload) {
35
+ gistId = undefined;
36
+ }
37
+ }
38
+ if (!gistId) {
39
+ const found = await (0, gistService_1.findSkillhubGist)(octokit);
40
+ if (found?.id) {
41
+ const foundId = found.id;
42
+ gistId = foundId;
43
+ await config_1.configStore.setGistId(foundId);
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.");
51
+ }
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
+ }
72
+ await applyUnionStrategy({
73
+ octokit,
74
+ gistId,
75
+ local: localPayload,
76
+ remote: resolvedRemote,
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];
148
+ }
149
+ async function tryReadSkillsLock(lockPath) {
150
+ try {
151
+ const raw = await promises_1.default.readFile(lockPath, "utf-8");
152
+ return parseSkillsLock(raw);
153
+ }
154
+ catch {
155
+ return null;
156
+ }
157
+ }
158
+ function parseSkillsLock(raw) {
159
+ const parsed = JSON.parse(raw);
160
+ // skills-lock.json에서 source 정보 추출 시도
161
+ const extractSkills = (items) => {
162
+ return items.map((item) => {
163
+ if (typeof item === "string") {
164
+ return { name: item, source: DEFAULT_SKILL_SOURCE_REPO };
165
+ }
166
+ if (typeof item === "object" && item !== null) {
167
+ const name = item.name || item.skill || String(item);
168
+ const source = item.source || item.repo || DEFAULT_SKILL_SOURCE_REPO;
169
+ return { name: String(name), source: String(source) };
170
+ }
171
+ return { name: String(item), source: DEFAULT_SKILL_SOURCE_REPO };
172
+ });
173
+ };
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
+ }
187
+ function parseSkillsListOutput(output) {
188
+ const cleaned = output.replace(/\x1b\[[0-9;]*m/g, "");
189
+ const lines = cleaned.split(/\r?\n/).map((line) => line.trim());
190
+ const skills = [];
191
+ for (const line of lines) {
192
+ if (!line)
193
+ continue;
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 });
216
+ }
217
+ return uniqueSortedSkills(skills);
218
+ }
219
+ function normalizeSkills(skills) {
220
+ const bannedSubstrings = [
221
+ "No global skills found",
222
+ "Try listing project skills without -g",
223
+ "No project skills found",
224
+ "Try listing global skills",
225
+ ];
226
+ const normalized = skills
227
+ .map((skill) => {
228
+ if (typeof skill === "string") {
229
+ return { name: skill, source: DEFAULT_SKILL_SOURCE_REPO };
230
+ }
231
+ return skill;
232
+ })
233
+ .filter((skill) => !!skill.name &&
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);
253
+ }
254
+ }
255
+ return result.sort((a, b) => {
256
+ if (a.source !== b.source) {
257
+ return a.source.localeCompare(b.source);
258
+ }
259
+ return a.name.localeCompare(b.name);
260
+ });
261
+ }
262
+ async function installSkills(skills) {
263
+ const succeeded = [];
264
+ const failed = [];
265
+ for (const skill of skills) {
266
+ try {
267
+ const { stdout, stderr } = await execAsync(`npx skills add ${skill.source} --skill "${skill.name}" --global --yes`);
268
+ const output = `${stdout ?? ""}\n${stderr ?? ""}`.trim();
269
+ if (output) {
270
+ console.log(output);
271
+ }
272
+ succeeded.push(skill);
273
+ }
274
+ catch (error) {
275
+ const stdout = error?.stdout ?? "";
276
+ const stderr = error?.stderr ?? "";
277
+ const reason = `${stdout}\n${stderr}`.trim() || String(error);
278
+ failed.push({ skill, reason });
279
+ console.warn([
280
+ `스킬 설치 실패: ${skill.name} (from ${skill.source})`,
281
+ reason && ` └─ ${reason}`,
282
+ ]
283
+ .filter(Boolean)
284
+ .join("\n"));
285
+ }
286
+ }
287
+ return { succeeded, failed };
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);
329
+ }
package/dist/index.js ADDED
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const commander_1 = require("commander");
4
+ const login_1 = require("./commands/login");
5
+ const sync_1 = require("./commands/sync");
6
+ const program = new commander_1.Command();
7
+ program.name("skillhub").description("SkillHub CLI").version("0.1.0");
8
+ program
9
+ .command("login")
10
+ .description("Login: register your GitHub PAT with gist access")
11
+ .action(async () => {
12
+ await (0, login_1.runLogin)();
13
+ });
14
+ program
15
+ .command("sync")
16
+ .description("Sync: reconcile local skills with remote Gist backup")
17
+ .option("-s, --strategy <strategy>", "merge strategy (union|latest)", "union")
18
+ .action(async (options) => {
19
+ await (0, sync_1.runSync)(options.strategy);
20
+ });
21
+ program.parseAsync(process.argv);
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.configStore = void 0;
4
+ let configPromise = null;
5
+ async function getConfig() {
6
+ if (!configPromise) {
7
+ configPromise = import("conf").then((module) => new module.default({ projectName: "skillhub" }));
8
+ }
9
+ return configPromise;
10
+ }
11
+ exports.configStore = {
12
+ async getToken() {
13
+ const config = await getConfig();
14
+ return config.get("githubToken");
15
+ },
16
+ async setToken(token) {
17
+ const config = await getConfig();
18
+ config.set("githubToken", token);
19
+ },
20
+ async getGistId() {
21
+ const config = await getConfig();
22
+ return config.get("gistId");
23
+ },
24
+ async setGistId(gistId) {
25
+ const config = await getConfig();
26
+ config.set("gistId", gistId);
27
+ },
28
+ };
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createOctokit = createOctokit;
4
+ exports.verifyToken = verifyToken;
5
+ exports.findSkillhubGist = findSkillhubGist;
6
+ exports.getSkillhubPayload = getSkillhubPayload;
7
+ exports.createSkillhubGist = createSkillhubGist;
8
+ exports.updateSkillhubGist = updateSkillhubGist;
9
+ const rest_1 = require("@octokit/rest");
10
+ const SKILLHUB_FILENAME = "skillhub.json";
11
+ function createOctokit(token) {
12
+ return new rest_1.Octokit({ auth: token });
13
+ }
14
+ async function verifyToken(token) {
15
+ const octokit = createOctokit(token);
16
+ await octokit.users.getAuthenticated();
17
+ // Also verify that the token can actually talk to the Gist API
18
+ // (this catches fine-grained tokens that don't have gist access).
19
+ await octokit.rest.gists.list({ per_page: 1 });
20
+ }
21
+ async function findSkillhubGist(octokit) {
22
+ const gists = await octokit.paginate(octokit.rest.gists.list, {
23
+ per_page: 100,
24
+ });
25
+ return gists.find((gist) => {
26
+ const files = Object.values(gist.files ?? {});
27
+ return files.some((file) => file?.filename === SKILLHUB_FILENAME);
28
+ });
29
+ }
30
+ async function getSkillhubPayload(octokit, gistId) {
31
+ const gist = await octokit.gists.get({ gist_id: gistId });
32
+ const file = Object.values(gist.data.files ?? {}).find((item) => item?.filename === SKILLHUB_FILENAME);
33
+ if (!file?.content) {
34
+ return null;
35
+ }
36
+ try {
37
+ const parsed = JSON.parse(file.content);
38
+ if (!Array.isArray(parsed.skills)) {
39
+ return null;
40
+ }
41
+ // 하위 호환성: string[] 형식을 SkillInfo[] 형식으로 변환
42
+ const normalizedSkills = parsed.skills.map((skill) => {
43
+ if (typeof skill === "string") {
44
+ return { name: skill, source: "vercel-labs/agent-skills" };
45
+ }
46
+ return skill;
47
+ });
48
+ return {
49
+ ...parsed,
50
+ skills: normalizedSkills,
51
+ };
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
57
+ async function createSkillhubGist(octokit, payload) {
58
+ const response = await octokit.gists.create({
59
+ description: "SkillHub sync",
60
+ public: false,
61
+ files: {
62
+ [SKILLHUB_FILENAME]: {
63
+ content: JSON.stringify(payload, null, 2),
64
+ },
65
+ },
66
+ });
67
+ return response.data;
68
+ }
69
+ async function updateSkillhubGist(octokit, gistId, payload) {
70
+ await octokit.gists.update({
71
+ gist_id: gistId,
72
+ files: {
73
+ [SKILLHUB_FILENAME]: {
74
+ content: JSON.stringify(payload, null, 2),
75
+ },
76
+ },
77
+ });
78
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@yonpark/skillhub-cli",
3
+ "version": "0.1.2",
4
+ "description": "SkillHub CLI - sync skills with GitHub Gist",
5
+ "bin": {
6
+ "skillhub": "bin/skillhub.js"
7
+ },
8
+ "main": "dist/index.js",
9
+ "type": "commonjs",
10
+ "scripts": {
11
+ "build": "tsc && tsc-alias",
12
+ "start": "node dist/index.js",
13
+ "prepare": "npm run build"
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "bin",
18
+ "README.md"
19
+ ],
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/yw9142/skillhub-cli.git"
23
+ },
24
+ "keywords": [
25
+ "cli",
26
+ "skills",
27
+ "agent",
28
+ "gist",
29
+ "sync"
30
+ ],
31
+ "author": "",
32
+ "license": "MIT",
33
+ "engines": {
34
+ "node": ">=18.0.0"
35
+ },
36
+ "dependencies": {
37
+ "@octokit/rest": "^20.0.2",
38
+ "commander": "^11.1.0",
39
+ "conf": "^12.0.0",
40
+ "inquirer": "^9.2.12"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^20.11.10",
44
+ "@types/inquirer": "^9.0.7",
45
+ "tsc-alias": "^1.8.8",
46
+ "typescript": "^5.3.3"
47
+ }
48
+ }