@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.
Files changed (42) hide show
  1. package/LICENSE +10 -18
  2. package/README.md +93 -36
  3. package/dist/auth.d.ts +1 -0
  4. package/dist/auth.js +19 -2
  5. package/dist/backends/gdrive.d.ts +6 -4
  6. package/dist/backends/gdrive.js +27 -13
  7. package/dist/backends/github.d.ts +10 -9
  8. package/dist/backends/github.js +71 -50
  9. package/dist/backends/interface.d.ts +19 -2
  10. package/dist/backends/local.d.ts +6 -4
  11. package/dist/backends/local.js +18 -13
  12. package/dist/backends/resolve.d.ts +2 -0
  13. package/dist/backends/resolve.js +25 -4
  14. package/dist/backends/routing.d.ts +38 -0
  15. package/dist/backends/routing.js +124 -0
  16. package/dist/commands/add.d.ts +3 -0
  17. package/dist/commands/add.js +130 -26
  18. package/dist/commands/collection.d.ts +1 -0
  19. package/dist/commands/collection.js +43 -37
  20. package/dist/commands/init.js +3 -3
  21. package/dist/commands/list.js +78 -8
  22. package/dist/commands/logout.d.ts +4 -0
  23. package/dist/commands/logout.js +35 -0
  24. package/dist/commands/refresh.js +1 -1
  25. package/dist/commands/registry.d.ts +1 -0
  26. package/dist/commands/registry.js +74 -36
  27. package/dist/commands/search.js +1 -1
  28. package/dist/commands/setup/github.d.ts +3 -0
  29. package/dist/commands/setup/github.js +8 -2
  30. package/dist/commands/setup/google.js +82 -42
  31. package/dist/commands/skill.d.ts +3 -0
  32. package/dist/commands/skill.js +76 -0
  33. package/dist/commands/status.d.ts +1 -0
  34. package/dist/commands/status.js +35 -0
  35. package/dist/config.js +6 -1
  36. package/dist/index.js +37 -3
  37. package/dist/registry.js +20 -8
  38. package/dist/types.d.ts +2 -0
  39. package/dist/utils/git.d.ts +10 -0
  40. package/dist/utils/git.js +27 -0
  41. package/package.json +2 -2
  42. package/skills/skillsmanager/SKILL.md +109 -6
package/LICENSE CHANGED
@@ -1,21 +1,13 @@
1
- MIT License
1
+ Copyright 2026 Ajay Prakash
2
2
 
3
- Copyright (c) 2026 Ajay Prakash
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
- 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:
7
+ http://www.apache.org/licenses/LICENSE-2.0
11
8
 
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.
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
- # Skills Manager
1
+ [![npm](https://img.shields.io/npm/v/%40skillsmanager%2Fcli)](https://www.npmjs.com/package/@skillsmanager/cli)
2
+ [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
3
+ [![Node >=18](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org)
4
+ [![CI](https://github.com/talktoajayprakash/skillsmanager/actions/workflows/release.yml/badge.svg)](https://github.com/talktoajayprakash/skillsmanager/actions)
5
+ [![Docs](https://img.shields.io/badge/docs-skillsmanager-blue)](https://talktoajayprakash.github.io/skillsmanager)
2
6
 
3
- A CLI for AI agents to discover, fetch, and share agent skills stored in Google Drive.
7
+ # Skills Manager CLI
4
8
 
5
- ## What is it?
9
+ **One place to manage, sync, and share all your AI agent skills — across every agent you use.**
6
10
 
7
- Skills Manager lets you maintain a personal library of agent skills in Google Drive and install them into any supported AI agent (Claude, Cursor, Windsurf, Copilot, etc.) with a single command.
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 are downloaded to a local cache (`~/.skillsmanager/cache/`) and symlinked into the agent's skills directory. No duplication one copy, many agents.
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
- ## Supported agents
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
- `claude`, `codex`, `cursor`, `windsurf`, `copilot`, `gemini`, `roo`, `agents`
17
+ ## Why Skills Manager?
14
18
 
15
- ## Installation
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
- ## Google Drive setup
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. Create a project → enable the **Google Drive API**
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
- ## Usage
59
+ Then authenticate and discover your registries:
29
60
 
30
61
  ```bash
31
- # Authenticate and discover registries
32
- skillsmanager init
62
+ skillsmanager setup google # walks you through OAuth
63
+ skillsmanager refresh # discovers collections in your Drive
64
+ ```
33
65
 
34
- # List all available skills
35
- skillsmanager list
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
- # Search skills by name or description
38
- skillsmanager search <query>
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
- # Download a skill and install it for an agent
41
- skillsmanager fetch <skill-name> --agent claude
93
+ Run tests:
42
94
 
43
- # Add a local skill to your registry
44
- skillsmanager add ./my-skill
95
+ ```bash
96
+ npm test
97
+ ```
45
98
 
46
- # Push local changes to an existing skill back to Drive
47
- skillsmanager update <skill-name>
99
+ To run without installing globally:
48
100
 
49
- # Re-scan Drive for new registries
50
- skillsmanager refresh
101
+ ```bash
102
+ node dist/index.js <command>
51
103
  ```
52
104
 
53
105
  ## Registry format
54
106
 
55
- Skills are indexed by a `SKILLS_SYNC.yaml` file inside any Google Drive folder you own:
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 containing a `SKILL.md` file with YAML frontmatter:
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 `SKILLS_SYNC.yaml` file owned by your Google account, so registries are found automatically on `skillsmanager init` or `skillsmanager refresh`.
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
- ## Design doc
139
+ ## License
83
140
 
84
- See [WRITEUP.md](./WRITEUP.md) for the full design.
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 = ["https://www.googleapis.com/auth/drive"];
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(folderName: string): Promise<CollectionInfo>;
15
+ createCollection({ name, skillsRepo }: import("./interface.js").CreateCollectionOptions): Promise<CollectionInfo>;
15
16
  deleteCollection(collection: CollectionInfo): Promise<void>;
16
- uploadSkill(collection: CollectionInfo, localPath: string, skillName: string): Promise<void>;
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(name?: string): Promise<RegistryInfo>;
23
+ createRegistry(options?: CreateRegistryOptions): Promise<RegistryInfo>;
22
24
  private findFolder;
23
25
  private uploadFolder;
24
26
  }
@@ -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(folderName) {
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 logicalName = folderName.replace(/^SKILLS_/i, "");
163
- const emptyCollection = { name: logicalName, owner, skills: [] };
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: logicalName,
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(name) {
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<void>;
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(name?: string, repoRef?: string): Promise<RegistryInfo>;
20
- createCollection(collectionName: string, repoRef?: string): Promise<CollectionInfo>;
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
  }
@@ -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
- async downloadSkill(collection, skillName, destDir) {
209
- const { repo } = parseRef(collection.folderId);
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 col = await this.readCollection(collection);
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 "${entry.path}" in repo "${repo}"`);
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
- async uploadSkill(collection, localPath, skillName) {
227
- const { repo } = parseRef(collection.folderId);
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(name, repoRef) {
309
- if (!repoRef)
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(repoRef);
312
- const workdir = this.ensureWorkdir(repoRef);
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 ?? (repoRef.split("/")[1] ?? "default"),
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: `${repoRef}:${metaDir}`,
331
- fileId: `${repoRef}:${metaDir}/${REGISTRY_FILENAME}`,
371
+ folderId: `${repo}:${metaDir}`,
372
+ fileId: `${repo}:${metaDir}/${REGISTRY_FILENAME}`,
332
373
  };
333
374
  }
334
375
  // ── createCollection ─────────────────────────────────────────────────────────
335
- async createCollection(collectionName, repoRef) {
336
- if (!repoRef)
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}/${collectionName}`;
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: collectionName, owner, skills: [] };
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 ${collectionName}`);
391
+ await this.commitAndPush(workdir, `chore: init collection ${name}`);
349
392
  return {
350
393
  id: randomUUID(),
351
- name: collectionName,
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
  }