@yonpark/skillhub-cli 0.1.3 → 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 CHANGED
@@ -1,185 +1,136 @@
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
+ ### Auth
28
+
29
+ ```bash
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
36
+ ```
37
+
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`)
41
+
42
+ ### Sync
43
+
44
+ `skillhub sync` requires a subcommand.
45
+
46
+ ```bash
47
+ skillhub sync pull
48
+ skillhub sync push
49
+ skillhub sync merge
50
+ skillhub sync auto
51
+ ```
52
+
53
+ Common options:
54
+
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:
61
+
62
+ ```bash
63
+ --yes # skip deletion confirmation prompt
64
+ ```
65
+
66
+ Mode behavior:
67
+
68
+ - `pull`: mirror remote to local (remote -> local). Installs missing local skills and removes extra local skills.
69
+ - `push`: mirror local to remote (local -> remote). Updates/creates remote Gist payload from local skills.
70
+ - `merge`: union local + remote, installs missing local skills, uploads when remote differs.
71
+ - `auto`: compare `remote.updatedAt` and local `lastSyncAt`; install from remote when remote is newer, otherwise upload local when needed.
72
+
73
+ ## Migration (Breaking Changes in 0.3.0)
74
+
75
+ Removed commands:
76
+
77
+ - `skillhub login`
78
+ - `skillhub status`
79
+ - `skillhub logout`
80
+ - `skillhub sync --strategy ...`
81
+ - `skillhub sync` (without a subcommand)
82
+
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`
90
+
91
+ ## Payload Format
92
+
93
+ `skillhub.json` in Gist:
94
+
95
+ ```json
96
+ {
97
+ "skills": [
98
+ { "name": "vercel-composition-patterns", "source": "vercel-labs/agent-skills" }
99
+ ],
100
+ "updatedAt": "2026-01-29T07:27:53.844Z"
101
+ }
102
+ ```
103
+
104
+ Backward compatibility for legacy `skills: string[]` payloads is preserved.
105
+
106
+ ## Local npm Credentials
107
+
108
+ If you need npm auth, copy `.npmrc.example` to `.npmrc` and keep it local only.
109
+
110
+ ## Release (Changesets)
111
+
112
+ Create release note:
113
+
114
+ ```bash
115
+ npm run changeset
116
+ ```
117
+
118
+ Version packages:
119
+
120
+ ```bash
121
+ npm run version-packages
122
+ ```
123
+
124
+ Publish:
125
+
126
+ ```bash
127
+ npm run release
128
+ ```
129
+
130
+ CI release workflow publishes to GitHub Packages only:
131
+
132
+ - GitHub Packages publish uses `GITHUB_TOKEN` by default.
133
+ - If needed, add `GH_PACKAGES_TOKEN` (PAT with `write:packages`) for GitHub Packages.
134
+ - GitHub Packages target package is `@yw9142/skillhub-cli`.
135
+
136
+ npmjs publish is manual.
@@ -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
+ }