@yonpark/skillhub-cli 0.1.3 → 0.2.0

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