@skillsmanager/cli 0.0.4 → 0.0.6
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 +10 -18
- package/README.md +93 -36
- package/dist/auth.d.ts +1 -0
- package/dist/auth.js +19 -2
- package/dist/backends/gdrive.d.ts +6 -4
- package/dist/backends/gdrive.js +27 -13
- package/dist/backends/github.d.ts +10 -9
- package/dist/backends/github.js +71 -50
- package/dist/backends/interface.d.ts +19 -2
- package/dist/backends/local.d.ts +6 -4
- package/dist/backends/local.js +18 -13
- package/dist/backends/resolve.d.ts +2 -0
- package/dist/backends/resolve.js +25 -4
- package/dist/backends/routing.d.ts +38 -0
- package/dist/backends/routing.js +124 -0
- package/dist/commands/add.d.ts +3 -0
- package/dist/commands/add.js +130 -26
- package/dist/commands/collection.d.ts +1 -0
- package/dist/commands/collection.js +43 -37
- package/dist/commands/init.js +3 -3
- package/dist/commands/list.js +78 -8
- package/dist/commands/logout.d.ts +4 -0
- package/dist/commands/logout.js +35 -0
- package/dist/commands/refresh.js +1 -1
- package/dist/commands/registry.d.ts +1 -0
- package/dist/commands/registry.js +74 -36
- package/dist/commands/search.js +1 -1
- package/dist/commands/setup/github.d.ts +3 -0
- package/dist/commands/setup/github.js +8 -2
- package/dist/commands/setup/google.js +82 -42
- package/dist/commands/skill.d.ts +3 -0
- package/dist/commands/skill.js +76 -0
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/status.js +35 -0
- package/dist/config.js +6 -1
- package/dist/index.js +37 -3
- package/dist/registry.js +20 -8
- package/dist/types.d.ts +2 -0
- package/dist/utils/git.d.ts +10 -0
- package/dist/utils/git.js +27 -0
- package/package.json +2 -2
- package/skills/skillsmanager/SKILL.md +109 -6
package/LICENSE
CHANGED
|
@@ -1,21 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
Copyright 2026 Ajay Prakash
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License.
|
|
5
|
+
You may obtain a copy of the License at
|
|
4
6
|
|
|
5
|
-
|
|
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:
|
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
11
8
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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.
|
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
See the License for the specific language governing permissions and
|
|
13
|
+
limitations under the License.
|
package/README.md
CHANGED
|
@@ -1,58 +1,110 @@
|
|
|
1
|
-
|
|
1
|
+
[](https://www.npmjs.com/package/@skillsmanager/cli)
|
|
2
|
+
[](LICENSE)
|
|
3
|
+
[](https://nodejs.org)
|
|
4
|
+
[](https://github.com/talktoajayprakash/skillsmanager/actions)
|
|
5
|
+
[](https://talktoajayprakash.github.io/skillsmanager)
|
|
2
6
|
|
|
3
|
-
|
|
7
|
+
# Skills Manager CLI
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
**One place to manage, sync, and share all your AI agent skills — across every agent you use.**
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
You build skills for your AI agents, but keeping track of them is a mess. They're scattered across GitHub repos, local folders, and machines. Each agent has its own directory. Nothing is searchable. Nothing is shared.
|
|
8
12
|
|
|
9
|
-
Skills
|
|
13
|
+
Skills Manager fixes this. It gives every skill a home — in Google Drive, GitHub, or any storage backend you choose — and makes them instantly available to any agent via a single CLI command. Your agents can search, fetch, and use any skill regardless of where it lives.
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
Build skills confidently, store them where you want, and sync them across every device and agent you work with — Claude, Cursor, OpenAI Codex, OpenClaw, and beyond.
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
## Why Skills Manager?
|
|
14
18
|
|
|
15
|
-
|
|
19
|
+
- **Unified skill library** — one searchable index across all your skills, wherever they're stored
|
|
20
|
+
- **Cross-agent** — install any skill into Claude, Cursor, Windsurf, Copilot, Gemini, and more
|
|
21
|
+
- **Backend-agnostic** — store in Google Drive, GitHub, Dropbox, AWS S3, or local filesystem
|
|
22
|
+
- **Sync across devices** — skills follow you, not your machine
|
|
23
|
+
- **No duplication** — cached once locally, symlinked into each agent's directory
|
|
24
|
+
- **Git-friendly** — plain Markdown files, easy to version-control and review
|
|
25
|
+
|
|
26
|
+
## Supported Agents
|
|
27
|
+
|
|
28
|
+
`claude` · `codex` · `cursor` · `windsurf` · `copilot` · `gemini` · `roo` · `openclaw` · `agents`
|
|
29
|
+
|
|
30
|
+
> **OpenClaw users:** OpenClaw's skill system uses the same `SKILL.md` format and directory-based loading that Skills Manager is built around. Your OpenClaw skills are first-class citizens — store them in any backend, search them, and sync them across devices just like any other skill.
|
|
31
|
+
>
|
|
32
|
+
> Any agent that reads from a skills directory works with Skills Manager. If your agent can read a file, it can use your skills.
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
### 1. Install
|
|
16
37
|
|
|
17
38
|
```bash
|
|
18
|
-
npm install -g skillsmanager
|
|
39
|
+
npm install -g @skillsmanager/cli
|
|
19
40
|
```
|
|
20
41
|
|
|
21
|
-
|
|
42
|
+
### 2. Install the skillsmanager skill (lets your agent drive Skills Manager)
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
skillsmanager install
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
This installs the bundled `skillsmanager` skill into all detected agents so your AI assistant can manage skills on your behalf.
|
|
49
|
+
|
|
50
|
+
### 3. One-time Google Drive setup
|
|
51
|
+
|
|
52
|
+
Skills Manager uses Google Drive as a remote registry. To connect it:
|
|
22
53
|
|
|
23
|
-
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
|
|
24
|
-
2.
|
|
25
|
-
3. Create OAuth 2.0 credentials (Desktop app)
|
|
54
|
+
1. Go to [Google Cloud Console](https://console.cloud.google.com/) and create a project
|
|
55
|
+
2. Enable the **Google Drive API** for that project
|
|
56
|
+
3. Create **OAuth 2.0 credentials** (Desktop app type)
|
|
26
57
|
4. Download `credentials.json` and save it to `~/.skillsmanager/credentials.json`
|
|
27
58
|
|
|
28
|
-
|
|
59
|
+
Then authenticate and discover your registries:
|
|
29
60
|
|
|
30
61
|
```bash
|
|
31
|
-
#
|
|
32
|
-
skillsmanager
|
|
62
|
+
skillsmanager setup google # walks you through OAuth
|
|
63
|
+
skillsmanager refresh # discovers collections in your Drive
|
|
64
|
+
```
|
|
33
65
|
|
|
34
|
-
|
|
35
|
-
|
|
66
|
+
## Commands
|
|
67
|
+
|
|
68
|
+
| Command | Description |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `skillsmanager install` | Install the skillsmanager skill to all agents |
|
|
71
|
+
| `skillsmanager list` | List all available skills |
|
|
72
|
+
| `skillsmanager search <query>` | Search skills by name or description |
|
|
73
|
+
| `skillsmanager fetch <name> --agent <agent>` | Download and install a skill for an agent |
|
|
74
|
+
| `skillsmanager add <path>` | Upload a local skill to a collection |
|
|
75
|
+
| `skillsmanager add --remote-path <path> --name <n> --description <d>` | Register a remote skill path (no upload) |
|
|
76
|
+
| `skillsmanager update <path>` | Push local changes back to remote storage |
|
|
77
|
+
| `skillsmanager refresh` | Re-discover collections from remote |
|
|
78
|
+
| `skillsmanager collection create [name] --backend github --repo <owner/repo>` | Create a collection in a GitHub repo |
|
|
79
|
+
| `skillsmanager collection create [name] --skills-repo <owner/repo>` | Create a collection with skills in a separate GitHub repo |
|
|
80
|
+
| `skillsmanager registry push --backend gdrive` | Push local registry to Google Drive |
|
|
81
|
+
| `skillsmanager registry push --backend github --repo <owner/repo>` | Push local registry to GitHub |
|
|
82
|
+
|
|
83
|
+
## Local Development
|
|
36
84
|
|
|
37
|
-
|
|
38
|
-
|
|
85
|
+
```bash
|
|
86
|
+
git clone https://github.com/talktoajayprakash/skillsmanager.git
|
|
87
|
+
cd skillsmanager
|
|
88
|
+
npm install
|
|
89
|
+
npm run build # compiles TypeScript to dist/
|
|
90
|
+
npm link # makes `skillsmanager` available globally from source
|
|
91
|
+
```
|
|
39
92
|
|
|
40
|
-
|
|
41
|
-
skillsmanager fetch <skill-name> --agent claude
|
|
93
|
+
Run tests:
|
|
42
94
|
|
|
43
|
-
|
|
44
|
-
|
|
95
|
+
```bash
|
|
96
|
+
npm test
|
|
97
|
+
```
|
|
45
98
|
|
|
46
|
-
|
|
47
|
-
skillsmanager update <skill-name>
|
|
99
|
+
To run without installing globally:
|
|
48
100
|
|
|
49
|
-
|
|
50
|
-
|
|
101
|
+
```bash
|
|
102
|
+
node dist/index.js <command>
|
|
51
103
|
```
|
|
52
104
|
|
|
53
105
|
## Registry format
|
|
54
106
|
|
|
55
|
-
Skills are indexed by a `
|
|
107
|
+
Skills are indexed by a `SKILLS_REGISTRY.yaml` file inside any Google Drive folder you own:
|
|
56
108
|
|
|
57
109
|
```yaml
|
|
58
110
|
name: my-skills
|
|
@@ -61,12 +113,9 @@ skills:
|
|
|
61
113
|
- name: code-review
|
|
62
114
|
path: code-review/
|
|
63
115
|
description: Reviews code for bugs, style, and security issues
|
|
64
|
-
- name: write-tests
|
|
65
|
-
path: write-tests/
|
|
66
|
-
description: Generates unit tests for a given function or module
|
|
67
116
|
```
|
|
68
117
|
|
|
69
|
-
Each skill is a directory
|
|
118
|
+
Each skill is a directory with a `SKILL.md` file:
|
|
70
119
|
|
|
71
120
|
```markdown
|
|
72
121
|
---
|
|
@@ -77,8 +126,16 @@ description: Reviews code for bugs, style, and security issues
|
|
|
77
126
|
... skill instructions ...
|
|
78
127
|
```
|
|
79
128
|
|
|
80
|
-
Skills Manager auto-discovers any `
|
|
129
|
+
Skills Manager auto-discovers any `SKILLS_REGISTRY.yaml` in your Google account on `refresh`.
|
|
130
|
+
|
|
131
|
+
## Contributing
|
|
132
|
+
|
|
133
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md) — PRs welcome.
|
|
134
|
+
|
|
135
|
+
## Security
|
|
136
|
+
|
|
137
|
+
See [SECURITY.md](./SECURITY.md) for how to report vulnerabilities.
|
|
81
138
|
|
|
82
|
-
##
|
|
139
|
+
## License
|
|
83
140
|
|
|
84
|
-
|
|
141
|
+
[Apache 2.0](LICENSE)
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { OAuth2Client } from "google-auth-library";
|
|
2
2
|
export declare function runAuthFlow(): Promise<OAuth2Client>;
|
|
3
|
+
export declare function getAuthedEmail(client: OAuth2Client): Promise<string | null>;
|
|
3
4
|
export declare function getAuthClient(): OAuth2Client;
|
|
4
5
|
export declare function hasToken(): boolean;
|
|
5
6
|
export declare function ensureAuth(): Promise<OAuth2Client>;
|
package/dist/auth.js
CHANGED
|
@@ -2,7 +2,11 @@ import fs from "fs";
|
|
|
2
2
|
import http from "http";
|
|
3
3
|
import { google } from "googleapis";
|
|
4
4
|
import { TOKEN_PATH, ensureConfigDir, readCredentials, credentialsExist } from "./config.js";
|
|
5
|
-
const SCOPES = [
|
|
5
|
+
const SCOPES = [
|
|
6
|
+
"https://www.googleapis.com/auth/drive",
|
|
7
|
+
"openid",
|
|
8
|
+
"https://www.googleapis.com/auth/userinfo.email",
|
|
9
|
+
];
|
|
6
10
|
const LOOPBACK_PORT = 3847;
|
|
7
11
|
const REDIRECT_URI = `http://localhost:${LOOPBACK_PORT}`;
|
|
8
12
|
function createOAuth2Client() {
|
|
@@ -26,6 +30,8 @@ function waitForAuthCode() {
|
|
|
26
30
|
if (error) {
|
|
27
31
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
28
32
|
res.end("<h2>Authorization failed.</h2><p>You can close this tab.</p>");
|
|
33
|
+
res.socket?.destroy();
|
|
34
|
+
server.closeAllConnections?.();
|
|
29
35
|
server.close();
|
|
30
36
|
reject(new Error(`OAuth error: ${error}`));
|
|
31
37
|
return;
|
|
@@ -33,6 +39,8 @@ function waitForAuthCode() {
|
|
|
33
39
|
if (code) {
|
|
34
40
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
35
41
|
res.end("<h2>Authorization successful!</h2><p>You can close this tab and return to the terminal.</p>");
|
|
42
|
+
res.socket?.destroy();
|
|
43
|
+
server.closeAllConnections?.();
|
|
36
44
|
server.close();
|
|
37
45
|
resolve(code);
|
|
38
46
|
}
|
|
@@ -59,9 +67,18 @@ export async function runAuthFlow() {
|
|
|
59
67
|
const { tokens } = await client.getToken(code);
|
|
60
68
|
client.setCredentials(tokens);
|
|
61
69
|
saveToken(client);
|
|
62
|
-
console.log("Authorization successful.");
|
|
63
70
|
return client;
|
|
64
71
|
}
|
|
72
|
+
export async function getAuthedEmail(client) {
|
|
73
|
+
try {
|
|
74
|
+
const oauth2 = google.oauth2({ version: "v2", auth: client });
|
|
75
|
+
const res = await oauth2.userinfo.get();
|
|
76
|
+
return res.data.email ?? null;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
65
82
|
export function getAuthClient() {
|
|
66
83
|
if (!fs.existsSync(TOKEN_PATH)) {
|
|
67
84
|
throw new Error(`Not authenticated. Run "skillsmanager init" first.`);
|
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
import type { OAuth2Client } from "google-auth-library";
|
|
2
|
-
import type { StorageBackend } from "./interface.js";
|
|
2
|
+
import type { BackendStatus, CreateRegistryOptions, StorageBackend } from "./interface.js";
|
|
3
3
|
import type { CollectionFile, CollectionInfo, RegistryCollectionRef, RegistryFile, RegistryInfo } from "../types.js";
|
|
4
4
|
export declare class GDriveBackend implements StorageBackend {
|
|
5
5
|
private drive;
|
|
6
6
|
constructor(auth: OAuth2Client);
|
|
7
7
|
getOwner(): Promise<string>;
|
|
8
8
|
getOwnerEmail(): Promise<string>;
|
|
9
|
+
getStatus(): Promise<BackendStatus>;
|
|
9
10
|
discoverCollections(): Promise<Omit<CollectionInfo, "id">[]>;
|
|
10
11
|
readCollection(collection: CollectionInfo): Promise<CollectionFile>;
|
|
11
12
|
writeCollection(collection: CollectionInfo, data: CollectionFile): Promise<void>;
|
|
12
13
|
downloadSkill(collection: CollectionInfo, skillName: string, destDir: string): Promise<void>;
|
|
13
14
|
private downloadFolder;
|
|
14
|
-
createCollection(
|
|
15
|
+
createCollection({ name, skillsRepo }: import("./interface.js").CreateCollectionOptions): Promise<CollectionInfo>;
|
|
15
16
|
deleteCollection(collection: CollectionInfo): Promise<void>;
|
|
16
|
-
|
|
17
|
+
deleteSkill(collection: CollectionInfo, skillName: string): Promise<void>;
|
|
18
|
+
uploadSkill(collection: CollectionInfo, localPath: string, skillName: string): Promise<string>;
|
|
17
19
|
discoverRegistries(): Promise<Omit<RegistryInfo, "id">[]>;
|
|
18
20
|
readRegistry(registry: RegistryInfo): Promise<RegistryFile>;
|
|
19
21
|
writeRegistry(registry: RegistryInfo, data: RegistryFile): Promise<void>;
|
|
20
22
|
resolveCollectionRef(ref: RegistryCollectionRef): Promise<Omit<CollectionInfo, "id"> | null>;
|
|
21
|
-
createRegistry(
|
|
23
|
+
createRegistry(options?: CreateRegistryOptions): Promise<RegistryInfo>;
|
|
22
24
|
private findFolder;
|
|
23
25
|
private uploadFolder;
|
|
24
26
|
}
|
package/dist/backends/gdrive.js
CHANGED
|
@@ -18,6 +18,14 @@ export class GDriveBackend {
|
|
|
18
18
|
async getOwnerEmail() {
|
|
19
19
|
return this.getOwner();
|
|
20
20
|
}
|
|
21
|
+
async getStatus() {
|
|
22
|
+
try {
|
|
23
|
+
return { loggedIn: true, identity: await this.getOwner() };
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return { loggedIn: false, identity: "", hint: "run: skillsmanager setup google" };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
21
29
|
// ── Collection operations ────────────────────────────────────────────────
|
|
22
30
|
async discoverCollections() {
|
|
23
31
|
const collections = [];
|
|
@@ -149,30 +157,28 @@ export class GDriveBackend {
|
|
|
149
157
|
}
|
|
150
158
|
} while (pageToken);
|
|
151
159
|
}
|
|
152
|
-
async createCollection(
|
|
160
|
+
async createCollection({ name, skillsRepo }) {
|
|
161
|
+
const folderName = `SKILLS_${name.toUpperCase()}`;
|
|
153
162
|
const folderRes = await this.drive.files.create({
|
|
154
|
-
requestBody: {
|
|
155
|
-
name: folderName,
|
|
156
|
-
mimeType: FOLDER_MIME,
|
|
157
|
-
},
|
|
163
|
+
requestBody: { name: folderName, mimeType: FOLDER_MIME },
|
|
158
164
|
fields: "id, name",
|
|
159
165
|
});
|
|
160
166
|
const folderId = folderRes.data.id;
|
|
161
167
|
const owner = await this.getOwnerEmail();
|
|
162
|
-
const
|
|
163
|
-
|
|
168
|
+
const emptyCollection = { name, owner, skills: [] };
|
|
169
|
+
if (skillsRepo) {
|
|
170
|
+
emptyCollection.type = "github";
|
|
171
|
+
emptyCollection.metadata = { repo: skillsRepo };
|
|
172
|
+
}
|
|
164
173
|
const content = serializeCollection(emptyCollection);
|
|
165
174
|
const fileRes = await this.drive.files.create({
|
|
166
|
-
requestBody: {
|
|
167
|
-
name: COLLECTION_FILENAME,
|
|
168
|
-
parents: [folderId],
|
|
169
|
-
},
|
|
175
|
+
requestBody: { name: COLLECTION_FILENAME, parents: [folderId] },
|
|
170
176
|
media: { mimeType: "text/yaml", body: Readable.from(content) },
|
|
171
177
|
fields: "id",
|
|
172
178
|
});
|
|
173
179
|
return {
|
|
174
180
|
id: randomUUID(),
|
|
175
|
-
name
|
|
181
|
+
name,
|
|
176
182
|
backend: "gdrive",
|
|
177
183
|
folderId,
|
|
178
184
|
registryFileId: fileRes.data.id ?? undefined,
|
|
@@ -185,6 +191,12 @@ export class GDriveBackend {
|
|
|
185
191
|
requestBody: { trashed: true },
|
|
186
192
|
});
|
|
187
193
|
}
|
|
194
|
+
async deleteSkill(collection, skillName) {
|
|
195
|
+
const folderId = await this.findFolder(skillName, collection.folderId);
|
|
196
|
+
if (folderId) {
|
|
197
|
+
await this.drive.files.update({ fileId: folderId, requestBody: { trashed: true } });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
188
200
|
async uploadSkill(collection, localPath, skillName) {
|
|
189
201
|
let folderId = await this.findFolder(skillName, collection.folderId);
|
|
190
202
|
if (!folderId) {
|
|
@@ -199,6 +211,7 @@ export class GDriveBackend {
|
|
|
199
211
|
folderId = res.data.id;
|
|
200
212
|
}
|
|
201
213
|
await this.uploadFolder(localPath, folderId);
|
|
214
|
+
return `${skillName}/`;
|
|
202
215
|
}
|
|
203
216
|
// ── Registry operations ──────────────────────────────────────────────────
|
|
204
217
|
async discoverRegistries() {
|
|
@@ -292,7 +305,8 @@ export class GDriveBackend {
|
|
|
292
305
|
}
|
|
293
306
|
return null;
|
|
294
307
|
}
|
|
295
|
-
async createRegistry(
|
|
308
|
+
async createRegistry(options) {
|
|
309
|
+
const name = options?.name;
|
|
296
310
|
const folderName = name ? `SKILLS_REGISTRY_${name}` : "SKILLS_REGISTRY";
|
|
297
311
|
const folderRes = await this.drive.files.create({
|
|
298
312
|
requestBody: { name: folderName, mimeType: FOLDER_MIME },
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { StorageBackend } from "./interface.js";
|
|
1
|
+
import type { BackendStatus, CreateRegistryOptions, StorageBackend } from "./interface.js";
|
|
2
2
|
import type { CollectionFile, CollectionInfo, RegistryCollectionRef, RegistryFile, RegistryInfo } from "../types.js";
|
|
3
3
|
export declare class GithubBackend implements StorageBackend {
|
|
4
4
|
getOwner(): Promise<string>;
|
|
5
|
+
getStatus(): Promise<BackendStatus>;
|
|
5
6
|
ensureRepo(repo: string): Promise<void>;
|
|
6
7
|
private ensureWorkdir;
|
|
7
8
|
private gitPushOrPR;
|
|
@@ -9,18 +10,18 @@ export declare class GithubBackend implements StorageBackend {
|
|
|
9
10
|
discoverCollections(): Promise<Omit<CollectionInfo, "id">[]>;
|
|
10
11
|
readCollection(collection: CollectionInfo): Promise<CollectionFile>;
|
|
11
12
|
writeCollection(collection: CollectionInfo, data: CollectionFile): Promise<void>;
|
|
13
|
+
/** Clone/pull repo and copy relPath to destDir. Usable by other backends for cross-backend routing. */
|
|
14
|
+
downloadSkillFromRepo(repo: string, relPath: string, destDir: string): Promise<void>;
|
|
15
|
+
/** Clone/pull repo and delete relPath. Usable by other backends for cross-backend routing. */
|
|
16
|
+
deleteSkillFromRepo(repo: string, relPath: string): Promise<void>;
|
|
12
17
|
downloadSkill(collection: CollectionInfo, skillName: string, destDir: string): Promise<void>;
|
|
13
|
-
uploadSkill(collection: CollectionInfo, localPath: string, skillName: string): Promise<
|
|
18
|
+
uploadSkill(collection: CollectionInfo, localPath: string, skillName: string): Promise<string>;
|
|
14
19
|
deleteCollection(collection: CollectionInfo): Promise<void>;
|
|
20
|
+
deleteSkill(collection: CollectionInfo, skillName: string): Promise<void>;
|
|
15
21
|
discoverRegistries(): Promise<Omit<RegistryInfo, "id">[]>;
|
|
16
22
|
readRegistry(registry: RegistryInfo): Promise<RegistryFile>;
|
|
17
23
|
writeRegistry(registry: RegistryInfo, data: RegistryFile): Promise<void>;
|
|
18
24
|
resolveCollectionRef(ref: RegistryCollectionRef): Promise<Omit<CollectionInfo, "id"> | null>;
|
|
19
|
-
createRegistry(
|
|
20
|
-
createCollection(
|
|
21
|
-
static detectRepoContext(absPath: string): {
|
|
22
|
-
repo: string;
|
|
23
|
-
repoRoot: string;
|
|
24
|
-
relPath: string;
|
|
25
|
-
} | null;
|
|
25
|
+
createRegistry(options?: CreateRegistryOptions): Promise<RegistryInfo>;
|
|
26
|
+
createCollection({ name, repo, skillsRepo }: import("./interface.js").CreateCollectionOptions): Promise<CollectionInfo>;
|
|
26
27
|
}
|
package/dist/backends/github.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from "path";
|
|
|
3
3
|
import { randomUUID } from "crypto";
|
|
4
4
|
import { spawnSync } from "child_process";
|
|
5
5
|
import chalk from "chalk";
|
|
6
|
+
import { ghInstalled, ghAuthed, ghGetLogin } from "../commands/setup/github.js";
|
|
6
7
|
import { parseCollection, serializeCollection, parseRegistryFile, serializeRegistryFile, COLLECTION_FILENAME, REGISTRY_FILENAME, } from "../registry.js";
|
|
7
8
|
import { CONFIG_DIR } from "../config.js";
|
|
8
9
|
const GITHUB_WORKDIR = path.join(CONFIG_DIR, "github-workdir");
|
|
@@ -20,6 +21,13 @@ function parseRef(folderId) {
|
|
|
20
21
|
function workdirFor(repo) {
|
|
21
22
|
return path.join(GITHUB_WORKDIR, repo.replace("/", "_"));
|
|
22
23
|
}
|
|
24
|
+
/** Returns the repo where skill files live — respects col.type + metadata.repo; defaults to host repo. */
|
|
25
|
+
function skillsRepo(col, hostRepo) {
|
|
26
|
+
if (col.type === "github" || col.type === undefined) {
|
|
27
|
+
return col.metadata?.repo ?? hostRepo;
|
|
28
|
+
}
|
|
29
|
+
return hostRepo;
|
|
30
|
+
}
|
|
23
31
|
// ── CLI helpers ───────────────────────────────────────────────────────────────
|
|
24
32
|
function ghExec(args, opts) {
|
|
25
33
|
const r = spawnSync("gh", args, {
|
|
@@ -64,6 +72,13 @@ export class GithubBackend {
|
|
|
64
72
|
}
|
|
65
73
|
return r.stdout;
|
|
66
74
|
}
|
|
75
|
+
async getStatus() {
|
|
76
|
+
if (!ghInstalled())
|
|
77
|
+
return { loggedIn: false, identity: "", hint: "install gh CLI first" };
|
|
78
|
+
if (!ghAuthed())
|
|
79
|
+
return { loggedIn: false, identity: "", hint: "run: skillsmanager setup github" };
|
|
80
|
+
return { loggedIn: true, identity: ghGetLogin() };
|
|
81
|
+
}
|
|
67
82
|
// ── Ensure repo exists (create private if not) ───────────────────────────────
|
|
68
83
|
async ensureRepo(repo) {
|
|
69
84
|
const check = ghExec(["api", `repos/${repo}`]);
|
|
@@ -205,39 +220,55 @@ export class GithubBackend {
|
|
|
205
220
|
gitExec(["add", path.join(metaDir, COLLECTION_FILENAME)], workdir);
|
|
206
221
|
await this.commitAndPush(workdir, `chore: update ${COLLECTION_FILENAME} for ${collection.name}`);
|
|
207
222
|
}
|
|
208
|
-
|
|
209
|
-
|
|
223
|
+
/** Clone/pull repo and copy relPath to destDir. Usable by other backends for cross-backend routing. */
|
|
224
|
+
async downloadSkillFromRepo(repo, relPath, destDir) {
|
|
210
225
|
const workdir = this.ensureWorkdir(repo);
|
|
211
|
-
// Refresh to get latest
|
|
212
226
|
gitExec(["pull", "--ff-only"], workdir);
|
|
213
|
-
const
|
|
214
|
-
const entry = col.skills.find((s) => s.name === skillName);
|
|
215
|
-
if (!entry) {
|
|
216
|
-
throw new Error(`Skill "${skillName}" not found in collection "${collection.name}"`);
|
|
217
|
-
}
|
|
218
|
-
const skillPath = path.join(workdir, entry.path);
|
|
227
|
+
const skillPath = path.join(workdir, relPath);
|
|
219
228
|
if (!fs.existsSync(skillPath)) {
|
|
220
|
-
throw new Error(`Skill directory not found at "${
|
|
229
|
+
throw new Error(`Skill directory not found at "${relPath}" in repo "${repo}"`);
|
|
221
230
|
}
|
|
222
231
|
if (path.resolve(skillPath) !== path.resolve(destDir)) {
|
|
223
232
|
copyDirSync(skillPath, destDir);
|
|
224
233
|
}
|
|
225
234
|
}
|
|
226
|
-
|
|
227
|
-
|
|
235
|
+
/** Clone/pull repo and delete relPath. Usable by other backends for cross-backend routing. */
|
|
236
|
+
async deleteSkillFromRepo(repo, relPath) {
|
|
228
237
|
const workdir = this.ensureWorkdir(repo);
|
|
238
|
+
gitExec(["pull", "--ff-only"], workdir);
|
|
239
|
+
const skillPath = path.join(workdir, relPath);
|
|
240
|
+
if (!fs.existsSync(skillPath))
|
|
241
|
+
return;
|
|
242
|
+
fs.rmSync(skillPath, { recursive: true, force: true });
|
|
243
|
+
gitExec(["add", "-A"], workdir);
|
|
244
|
+
await this.commitAndPush(workdir, `chore: remove skill at ${relPath}`);
|
|
245
|
+
}
|
|
246
|
+
async downloadSkill(collection, skillName, destDir) {
|
|
247
|
+
const { repo: hostRepo } = parseRef(collection.folderId);
|
|
248
|
+
const col = await this.readCollection(collection);
|
|
249
|
+
const entry = col.skills.find((s) => s.name === skillName);
|
|
250
|
+
if (!entry) {
|
|
251
|
+
throw new Error(`Skill "${skillName}" not found in collection "${collection.name}"`);
|
|
252
|
+
}
|
|
253
|
+
const srcRepo = skillsRepo(col, hostRepo);
|
|
254
|
+
await this.downloadSkillFromRepo(srcRepo, entry.path, destDir);
|
|
255
|
+
}
|
|
256
|
+
async uploadSkill(collection, localPath, skillName) {
|
|
257
|
+
const { repo: hostRepo } = parseRef(collection.folderId);
|
|
258
|
+
const workdir = this.ensureWorkdir(hostRepo);
|
|
229
259
|
const resolvedLocal = path.resolve(localPath);
|
|
230
260
|
const resolvedWorkdir = path.resolve(workdir);
|
|
231
261
|
if (resolvedLocal.startsWith(resolvedWorkdir + path.sep) ||
|
|
232
262
|
resolvedLocal === resolvedWorkdir) {
|
|
233
|
-
// Already in the repo — no copy needed
|
|
234
|
-
return;
|
|
263
|
+
// Already in the repo — no copy needed; return relative path from workdir
|
|
264
|
+
return path.relative(workdir, resolvedLocal).replace(/\\/g, "/");
|
|
235
265
|
}
|
|
236
266
|
// External skill: copy into .agentskills/<skillName>/ in the repo
|
|
237
267
|
const dest = path.join(workdir, ".agentskills", skillName);
|
|
238
268
|
copyDirSync(localPath, dest);
|
|
239
269
|
gitExec(["add", path.join(".agentskills", skillName)], workdir);
|
|
240
270
|
await this.commitAndPush(workdir, `chore: add skill ${skillName}`);
|
|
271
|
+
return `.agentskills/${skillName}`;
|
|
241
272
|
}
|
|
242
273
|
async deleteCollection(collection) {
|
|
243
274
|
const { repo, metaDir } = parseRef(collection.folderId);
|
|
@@ -249,6 +280,15 @@ export class GithubBackend {
|
|
|
249
280
|
gitExec(["add", "-A"], workdir);
|
|
250
281
|
await this.commitAndPush(workdir, `chore: remove collection ${collection.name}`);
|
|
251
282
|
}
|
|
283
|
+
async deleteSkill(collection, skillName) {
|
|
284
|
+
const { repo: hostRepo } = parseRef(collection.folderId);
|
|
285
|
+
const col = await this.readCollection(collection);
|
|
286
|
+
const entry = col.skills.find((s) => s.name === skillName);
|
|
287
|
+
if (!entry)
|
|
288
|
+
return;
|
|
289
|
+
const srcRepo = skillsRepo(col, hostRepo);
|
|
290
|
+
await this.deleteSkillFromRepo(srcRepo, entry.path);
|
|
291
|
+
}
|
|
252
292
|
// ── Registry operations ───────────────────────────────────────────────────────
|
|
253
293
|
async discoverRegistries() {
|
|
254
294
|
const r = ghExec(["repo", "list", "--json", "nameWithOwner", "--limit", "100"]);
|
|
@@ -305,17 +345,18 @@ export class GithubBackend {
|
|
|
305
345
|
return null;
|
|
306
346
|
return { name: ref.name, backend: "github", folderId: ref.ref };
|
|
307
347
|
}
|
|
308
|
-
async createRegistry(
|
|
309
|
-
|
|
348
|
+
async createRegistry(options) {
|
|
349
|
+
const { name, repo } = options ?? {};
|
|
350
|
+
if (!repo)
|
|
310
351
|
throw new Error("GitHub backend requires --repo <owner/repo>");
|
|
311
|
-
await this.ensureRepo(
|
|
312
|
-
const workdir = this.ensureWorkdir(
|
|
352
|
+
await this.ensureRepo(repo);
|
|
353
|
+
const workdir = this.ensureWorkdir(repo);
|
|
313
354
|
const metaDir = SKILLSMANAGER_DIR;
|
|
314
355
|
const filePath = path.join(workdir, metaDir, REGISTRY_FILENAME);
|
|
315
356
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
316
357
|
const owner = await this.getOwner();
|
|
317
358
|
const registryData = {
|
|
318
|
-
name: name ?? (
|
|
359
|
+
name: name ?? (repo.split("/")[1] ?? "default"),
|
|
319
360
|
owner,
|
|
320
361
|
source: "github",
|
|
321
362
|
collections: [],
|
|
@@ -327,52 +368,32 @@ export class GithubBackend {
|
|
|
327
368
|
id: randomUUID(),
|
|
328
369
|
name: registryData.name,
|
|
329
370
|
backend: "github",
|
|
330
|
-
folderId: `${
|
|
331
|
-
fileId: `${
|
|
371
|
+
folderId: `${repo}:${metaDir}`,
|
|
372
|
+
fileId: `${repo}:${metaDir}/${REGISTRY_FILENAME}`,
|
|
332
373
|
};
|
|
333
374
|
}
|
|
334
375
|
// ── createCollection ─────────────────────────────────────────────────────────
|
|
335
|
-
async createCollection(
|
|
336
|
-
if (!
|
|
376
|
+
async createCollection({ name, repo, skillsRepo }) {
|
|
377
|
+
if (!repo)
|
|
337
378
|
throw new Error("GitHub backend requires --repo <owner/repo>");
|
|
338
|
-
const repo = repoRef;
|
|
339
379
|
await this.ensureRepo(repo);
|
|
340
380
|
const workdir = this.ensureWorkdir(repo);
|
|
341
|
-
const metaDir = `${SKILLSMANAGER_DIR}/${
|
|
381
|
+
const metaDir = `${SKILLSMANAGER_DIR}/${name}`;
|
|
342
382
|
const filePath = path.join(workdir, metaDir, COLLECTION_FILENAME);
|
|
343
383
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
344
384
|
const owner = await this.getOwner();
|
|
345
|
-
const colData = { name
|
|
385
|
+
const colData = { name, owner, skills: [] };
|
|
386
|
+
if (skillsRepo && skillsRepo !== repo) {
|
|
387
|
+
colData.metadata = { repo: skillsRepo };
|
|
388
|
+
}
|
|
346
389
|
fs.writeFileSync(filePath, serializeCollection(colData));
|
|
347
390
|
gitExec(["add", path.join(metaDir, COLLECTION_FILENAME)], workdir);
|
|
348
|
-
await this.commitAndPush(workdir, `chore: init collection ${
|
|
391
|
+
await this.commitAndPush(workdir, `chore: init collection ${name}`);
|
|
349
392
|
return {
|
|
350
393
|
id: randomUUID(),
|
|
351
|
-
name
|
|
394
|
+
name,
|
|
352
395
|
backend: "github",
|
|
353
396
|
folderId: `${repo}:${metaDir}`,
|
|
354
397
|
};
|
|
355
398
|
}
|
|
356
|
-
// ── Static: detect if a path is inside a GitHub-tracked git repo ──────────────
|
|
357
|
-
static detectRepoContext(absPath) {
|
|
358
|
-
const rootResult = spawnSync("git", ["-C", absPath, "rev-parse", "--show-toplevel"], {
|
|
359
|
-
encoding: "utf-8", stdio: "pipe",
|
|
360
|
-
});
|
|
361
|
-
if (rootResult.status !== 0)
|
|
362
|
-
return null;
|
|
363
|
-
const repoRoot = rootResult.stdout.trim();
|
|
364
|
-
const remoteResult = spawnSync("git", ["-C", repoRoot, "remote", "get-url", "origin"], {
|
|
365
|
-
encoding: "utf-8", stdio: "pipe",
|
|
366
|
-
});
|
|
367
|
-
if (remoteResult.status !== 0)
|
|
368
|
-
return null;
|
|
369
|
-
const remoteUrl = remoteResult.stdout.trim();
|
|
370
|
-
const match = remoteUrl.match(/github\.com[/:]([^/]+\/[^/]+?)(?:\.git)?$/) ??
|
|
371
|
-
remoteUrl.match(/github\.com\/([^/]+\/[^/]+)/);
|
|
372
|
-
if (!match)
|
|
373
|
-
return null;
|
|
374
|
-
const repo = match[1].replace(/\.git$/, "");
|
|
375
|
-
const relPath = path.relative(repoRoot, absPath).replace(/\\/g, "/");
|
|
376
|
-
return { repo, repoRoot, relPath };
|
|
377
|
-
}
|
|
378
399
|
}
|