@skillsmanager/cli 0.0.4 → 0.0.5
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 +89 -35
- package/dist/auth.d.ts +1 -0
- package/dist/auth.js +19 -2
- package/dist/backends/gdrive.d.ts +1 -0
- package/dist/backends/gdrive.js +6 -0
- package/dist/backends/github.d.ts +2 -1
- package/dist/backends/github.js +37 -8
- package/dist/backends/interface.d.ts +1 -0
- package/dist/backends/local.d.ts +1 -0
- package/dist/backends/local.js +6 -0
- package/dist/commands/add.d.ts +3 -0
- package/dist/commands/add.js +121 -1
- package/dist/commands/collection.d.ts +1 -0
- package/dist/commands/collection.js +44 -4
- package/dist/commands/logout.d.ts +4 -0
- package/dist/commands/logout.js +35 -0
- package/dist/commands/registry.d.ts +1 -0
- package/dist/commands/registry.js +68 -16
- 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 +65 -0
- package/dist/index.js +37 -3
- package/dist/registry.js +11 -3
- package/dist/types.d.ts +1 -0
- package/package.json +2 -2
- package/skills/skillsmanager/SKILL.md +38 -3
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,107 @@
|
|
|
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)
|
|
6
|
+
|
|
1
7
|
# Skills Manager
|
|
2
8
|
|
|
3
|
-
|
|
9
|
+
**One place to manage, sync, and share all your AI agent skills — across every agent you use.**
|
|
10
|
+
|
|
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.
|
|
12
|
+
|
|
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.
|
|
14
|
+
|
|
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.
|
|
16
|
+
|
|
17
|
+
## Why Skills Manager?
|
|
18
|
+
|
|
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
|
|
4
25
|
|
|
5
|
-
##
|
|
26
|
+
## Supported Agents
|
|
6
27
|
|
|
7
|
-
|
|
28
|
+
`claude` · `codex` · `cursor` · `windsurf` · `copilot` · `gemini` · `roo` · `openclaw` · `agents`
|
|
8
29
|
|
|
9
|
-
|
|
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.
|
|
10
33
|
|
|
11
|
-
##
|
|
34
|
+
## Quick Start
|
|
12
35
|
|
|
13
|
-
|
|
36
|
+
### 1. Install
|
|
14
37
|
|
|
15
|
-
|
|
38
|
+
```bash
|
|
39
|
+
npm install -g @skillsmanager/cli
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 2. Install the skillsmanager skill (lets your agent drive Skills Manager)
|
|
16
43
|
|
|
17
44
|
```bash
|
|
18
|
-
|
|
45
|
+
skillsmanager install
|
|
19
46
|
```
|
|
20
47
|
|
|
21
|
-
|
|
48
|
+
This installs the bundled `skillsmanager` skill into all detected agents so your AI assistant can manage skills on your behalf.
|
|
22
49
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
50
|
+
### 3. One-time Google Drive setup
|
|
51
|
+
|
|
52
|
+
Skills Manager uses Google Drive as a remote registry. To connect it:
|
|
53
|
+
|
|
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
|
-
skillsmanager list
|
|
66
|
+
## Commands
|
|
36
67
|
|
|
37
|
-
|
|
38
|
-
|
|
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 update <path>` | Push local changes back to remote storage |
|
|
76
|
+
| `skillsmanager refresh` | Re-discover collections from remote |
|
|
77
|
+
| `skillsmanager collection create` | Create a new skill collection |
|
|
78
|
+
| `skillsmanager registry push --backend gdrive` | Push local registry to Google Drive |
|
|
39
79
|
|
|
40
|
-
|
|
41
|
-
skillsmanager fetch <skill-name> --agent claude
|
|
80
|
+
## Local Development
|
|
42
81
|
|
|
43
|
-
|
|
44
|
-
|
|
82
|
+
```bash
|
|
83
|
+
git clone https://github.com/talktoajayprakash/skillsmanager.git
|
|
84
|
+
cd skillsmanager
|
|
85
|
+
npm install
|
|
86
|
+
npm run build # compiles TypeScript to dist/
|
|
87
|
+
npm link # makes `skillsmanager` available globally from source
|
|
88
|
+
```
|
|
45
89
|
|
|
46
|
-
|
|
47
|
-
skillsmanager update <skill-name>
|
|
90
|
+
Run tests:
|
|
48
91
|
|
|
49
|
-
|
|
50
|
-
|
|
92
|
+
```bash
|
|
93
|
+
npm test
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
To run without installing globally:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
node dist/index.js <command>
|
|
51
100
|
```
|
|
52
101
|
|
|
53
102
|
## Registry format
|
|
54
103
|
|
|
55
|
-
Skills are indexed by a `
|
|
104
|
+
Skills are indexed by a `SKILLS_REGISTRY.yaml` file inside any Google Drive folder you own:
|
|
56
105
|
|
|
57
106
|
```yaml
|
|
58
107
|
name: my-skills
|
|
@@ -61,12 +110,9 @@ skills:
|
|
|
61
110
|
- name: code-review
|
|
62
111
|
path: code-review/
|
|
63
112
|
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
113
|
```
|
|
68
114
|
|
|
69
|
-
Each skill is a directory
|
|
115
|
+
Each skill is a directory with a `SKILL.md` file:
|
|
70
116
|
|
|
71
117
|
```markdown
|
|
72
118
|
---
|
|
@@ -77,8 +123,16 @@ description: Reviews code for bugs, style, and security issues
|
|
|
77
123
|
... skill instructions ...
|
|
78
124
|
```
|
|
79
125
|
|
|
80
|
-
Skills Manager auto-discovers any `
|
|
126
|
+
Skills Manager auto-discovers any `SKILLS_REGISTRY.yaml` in your Google account on `refresh`.
|
|
127
|
+
|
|
128
|
+
## Contributing
|
|
129
|
+
|
|
130
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md) — PRs welcome.
|
|
131
|
+
|
|
132
|
+
## Security
|
|
133
|
+
|
|
134
|
+
See [SECURITY.md](./SECURITY.md) for how to report vulnerabilities.
|
|
81
135
|
|
|
82
|
-
##
|
|
136
|
+
## License
|
|
83
137
|
|
|
84
|
-
|
|
138
|
+
[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.`);
|
|
@@ -13,6 +13,7 @@ export declare class GDriveBackend implements StorageBackend {
|
|
|
13
13
|
private downloadFolder;
|
|
14
14
|
createCollection(folderName: string): Promise<CollectionInfo>;
|
|
15
15
|
deleteCollection(collection: CollectionInfo): Promise<void>;
|
|
16
|
+
deleteSkill(collection: CollectionInfo, skillName: string): Promise<void>;
|
|
16
17
|
uploadSkill(collection: CollectionInfo, localPath: string, skillName: string): Promise<void>;
|
|
17
18
|
discoverRegistries(): Promise<Omit<RegistryInfo, "id">[]>;
|
|
18
19
|
readRegistry(registry: RegistryInfo): Promise<RegistryFile>;
|
package/dist/backends/gdrive.js
CHANGED
|
@@ -185,6 +185,12 @@ export class GDriveBackend {
|
|
|
185
185
|
requestBody: { trashed: true },
|
|
186
186
|
});
|
|
187
187
|
}
|
|
188
|
+
async deleteSkill(collection, skillName) {
|
|
189
|
+
const folderId = await this.findFolder(skillName, collection.folderId);
|
|
190
|
+
if (folderId) {
|
|
191
|
+
await this.drive.files.update({ fileId: folderId, requestBody: { trashed: true } });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
188
194
|
async uploadSkill(collection, localPath, skillName) {
|
|
189
195
|
let folderId = await this.findFolder(skillName, collection.folderId);
|
|
190
196
|
if (!folderId) {
|
|
@@ -12,12 +12,13 @@ export declare class GithubBackend implements StorageBackend {
|
|
|
12
12
|
downloadSkill(collection: CollectionInfo, skillName: string, destDir: string): Promise<void>;
|
|
13
13
|
uploadSkill(collection: CollectionInfo, localPath: string, skillName: string): Promise<void>;
|
|
14
14
|
deleteCollection(collection: CollectionInfo): Promise<void>;
|
|
15
|
+
deleteSkill(collection: CollectionInfo, skillName: string): Promise<void>;
|
|
15
16
|
discoverRegistries(): Promise<Omit<RegistryInfo, "id">[]>;
|
|
16
17
|
readRegistry(registry: RegistryInfo): Promise<RegistryFile>;
|
|
17
18
|
writeRegistry(registry: RegistryInfo, data: RegistryFile): Promise<void>;
|
|
18
19
|
resolveCollectionRef(ref: RegistryCollectionRef): Promise<Omit<CollectionInfo, "id"> | null>;
|
|
19
20
|
createRegistry(name?: string, repoRef?: string): Promise<RegistryInfo>;
|
|
20
|
-
createCollection(collectionName: string, repoRef?: string): Promise<CollectionInfo>;
|
|
21
|
+
createCollection(collectionName: string, repoRef?: string, skillsRepoRef?: string): Promise<CollectionInfo>;
|
|
21
22
|
static detectRepoContext(absPath: string): {
|
|
22
23
|
repo: string;
|
|
23
24
|
repoRoot: string;
|
package/dist/backends/github.js
CHANGED
|
@@ -20,6 +20,10 @@ function parseRef(folderId) {
|
|
|
20
20
|
function workdirFor(repo) {
|
|
21
21
|
return path.join(GITHUB_WORKDIR, repo.replace("/", "_"));
|
|
22
22
|
}
|
|
23
|
+
/** Returns the repo where skill files live — defaults to the collection host repo. */
|
|
24
|
+
function skillsRepo(col, hostRepo) {
|
|
25
|
+
return col.metadata?.repo ?? hostRepo;
|
|
26
|
+
}
|
|
23
27
|
// ── CLI helpers ───────────────────────────────────────────────────────────────
|
|
24
28
|
function ghExec(args, opts) {
|
|
25
29
|
const r = spawnSync("gh", args, {
|
|
@@ -206,26 +210,33 @@ export class GithubBackend {
|
|
|
206
210
|
await this.commitAndPush(workdir, `chore: update ${COLLECTION_FILENAME} for ${collection.name}`);
|
|
207
211
|
}
|
|
208
212
|
async downloadSkill(collection, skillName, destDir) {
|
|
209
|
-
const { repo } = parseRef(collection.folderId);
|
|
210
|
-
const workdir = this.ensureWorkdir(repo);
|
|
211
|
-
// Refresh to get latest
|
|
212
|
-
gitExec(["pull", "--ff-only"], workdir);
|
|
213
|
+
const { repo: hostRepo } = parseRef(collection.folderId);
|
|
213
214
|
const col = await this.readCollection(collection);
|
|
214
215
|
const entry = col.skills.find((s) => s.name === skillName);
|
|
215
216
|
if (!entry) {
|
|
216
217
|
throw new Error(`Skill "${skillName}" not found in collection "${collection.name}"`);
|
|
217
218
|
}
|
|
219
|
+
const srcRepo = skillsRepo(col, hostRepo);
|
|
220
|
+
const workdir = this.ensureWorkdir(srcRepo);
|
|
221
|
+
gitExec(["pull", "--ff-only"], workdir);
|
|
218
222
|
const skillPath = path.join(workdir, entry.path);
|
|
219
223
|
if (!fs.existsSync(skillPath)) {
|
|
220
|
-
throw new Error(`Skill directory not found at "${entry.path}" in repo "${
|
|
224
|
+
throw new Error(`Skill directory not found at "${entry.path}" in repo "${srcRepo}"`);
|
|
221
225
|
}
|
|
222
226
|
if (path.resolve(skillPath) !== path.resolve(destDir)) {
|
|
223
227
|
copyDirSync(skillPath, destDir);
|
|
224
228
|
}
|
|
225
229
|
}
|
|
226
230
|
async uploadSkill(collection, localPath, skillName) {
|
|
227
|
-
const { repo } = parseRef(collection.folderId);
|
|
228
|
-
|
|
231
|
+
const { repo: hostRepo } = parseRef(collection.folderId);
|
|
232
|
+
// If collection points to a foreign skills repo, we can't upload there
|
|
233
|
+
const col = await this.readCollection(collection);
|
|
234
|
+
const foreign = col.metadata?.repo;
|
|
235
|
+
if (foreign && foreign !== hostRepo) {
|
|
236
|
+
throw new Error(`Cannot upload skill to collection "${collection.name}": its skills source is "${foreign}" (a repo you may not own). ` +
|
|
237
|
+
`Use --remote-path to register a skill path from that repo instead.`);
|
|
238
|
+
}
|
|
239
|
+
const workdir = this.ensureWorkdir(hostRepo);
|
|
229
240
|
const resolvedLocal = path.resolve(localPath);
|
|
230
241
|
const resolvedWorkdir = path.resolve(workdir);
|
|
231
242
|
if (resolvedLocal.startsWith(resolvedWorkdir + path.sep) ||
|
|
@@ -249,6 +260,21 @@ export class GithubBackend {
|
|
|
249
260
|
gitExec(["add", "-A"], workdir);
|
|
250
261
|
await this.commitAndPush(workdir, `chore: remove collection ${collection.name}`);
|
|
251
262
|
}
|
|
263
|
+
async deleteSkill(collection, skillName) {
|
|
264
|
+
const { repo: hostRepo } = parseRef(collection.folderId);
|
|
265
|
+
const col = await this.readCollection(collection);
|
|
266
|
+
const entry = col.skills.find((s) => s.name === skillName);
|
|
267
|
+
if (!entry)
|
|
268
|
+
return;
|
|
269
|
+
const srcRepo = skillsRepo(col, hostRepo);
|
|
270
|
+
const workdir = this.ensureWorkdir(srcRepo);
|
|
271
|
+
const skillPath = path.join(workdir, entry.path);
|
|
272
|
+
if (!fs.existsSync(skillPath))
|
|
273
|
+
return;
|
|
274
|
+
fs.rmSync(skillPath, { recursive: true, force: true });
|
|
275
|
+
gitExec(["add", "-A"], workdir);
|
|
276
|
+
await this.commitAndPush(workdir, `chore: remove skill ${skillName}`);
|
|
277
|
+
}
|
|
252
278
|
// ── Registry operations ───────────────────────────────────────────────────────
|
|
253
279
|
async discoverRegistries() {
|
|
254
280
|
const r = ghExec(["repo", "list", "--json", "nameWithOwner", "--limit", "100"]);
|
|
@@ -332,7 +358,7 @@ export class GithubBackend {
|
|
|
332
358
|
};
|
|
333
359
|
}
|
|
334
360
|
// ── createCollection ─────────────────────────────────────────────────────────
|
|
335
|
-
async createCollection(collectionName, repoRef) {
|
|
361
|
+
async createCollection(collectionName, repoRef, skillsRepoRef) {
|
|
336
362
|
if (!repoRef)
|
|
337
363
|
throw new Error("GitHub backend requires --repo <owner/repo>");
|
|
338
364
|
const repo = repoRef;
|
|
@@ -343,6 +369,9 @@ export class GithubBackend {
|
|
|
343
369
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
344
370
|
const owner = await this.getOwner();
|
|
345
371
|
const colData = { name: collectionName, owner, skills: [] };
|
|
372
|
+
if (skillsRepoRef && skillsRepoRef !== repo) {
|
|
373
|
+
colData.metadata = { repo: skillsRepoRef };
|
|
374
|
+
}
|
|
346
375
|
fs.writeFileSync(filePath, serializeCollection(colData));
|
|
347
376
|
gitExec(["add", path.join(metaDir, COLLECTION_FILENAME)], workdir);
|
|
348
377
|
await this.commitAndPush(workdir, `chore: init collection ${collectionName}`);
|
|
@@ -7,6 +7,7 @@ export interface StorageBackend {
|
|
|
7
7
|
downloadSkill(collection: CollectionInfo, skillName: string, destDir: string): Promise<void>;
|
|
8
8
|
uploadSkill(collection: CollectionInfo, localPath: string, skillName: string): Promise<void>;
|
|
9
9
|
deleteCollection(collection: CollectionInfo): Promise<void>;
|
|
10
|
+
deleteSkill(collection: CollectionInfo, skillName: string): Promise<void>;
|
|
10
11
|
discoverRegistries(): Promise<Omit<RegistryInfo, "id">[]>;
|
|
11
12
|
readRegistry(registry: RegistryInfo): Promise<RegistryFile>;
|
|
12
13
|
writeRegistry(registry: RegistryInfo, data: RegistryFile): Promise<void>;
|
package/dist/backends/local.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export declare class LocalBackend implements StorageBackend {
|
|
|
12
12
|
downloadSkill(collection: CollectionInfo, skillName: string, destDir: string): Promise<void>;
|
|
13
13
|
uploadSkill(collection: CollectionInfo, localPath: string, skillName: string): Promise<void>;
|
|
14
14
|
deleteCollection(collection: CollectionInfo): Promise<void>;
|
|
15
|
+
deleteSkill(collection: CollectionInfo, skillName: string): Promise<void>;
|
|
15
16
|
discoverRegistries(): Promise<Omit<RegistryInfo, "id">[]>;
|
|
16
17
|
readRegistry(registry: RegistryInfo): Promise<RegistryFile>;
|
|
17
18
|
writeRegistry(registry: RegistryInfo, data: RegistryFile): Promise<void>;
|
package/dist/backends/local.js
CHANGED
|
@@ -78,6 +78,12 @@ export class LocalBackend {
|
|
|
78
78
|
fs.rmSync(collection.folderId, { recursive: true, force: true });
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
|
+
async deleteSkill(collection, skillName) {
|
|
82
|
+
const skillPath = path.join(collection.folderId, skillName);
|
|
83
|
+
if (fs.existsSync(skillPath)) {
|
|
84
|
+
fs.rmSync(skillPath, { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
81
87
|
// ── Registry operations ──────────────────────────────────────────────────
|
|
82
88
|
async discoverRegistries() {
|
|
83
89
|
if (!fs.existsSync(LOCAL_REGISTRY_PATH))
|
package/dist/commands/add.d.ts
CHANGED
package/dist/commands/add.js
CHANGED
|
@@ -7,6 +7,11 @@ import { readConfig, trackSkill } from "../config.js";
|
|
|
7
7
|
import { GithubBackend } from "../backends/github.js";
|
|
8
8
|
import { resolveBackend } from "../backends/resolve.js";
|
|
9
9
|
export async function addCommand(skillPath, options) {
|
|
10
|
+
// ── Remote-path mode: register a skill from a foreign repo without local files ─
|
|
11
|
+
if (options.remotePath) {
|
|
12
|
+
await addRemotePath(options);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
10
15
|
const absPath = path.resolve(skillPath);
|
|
11
16
|
if (!fs.existsSync(absPath) || !fs.statSync(absPath).isDirectory()) {
|
|
12
17
|
console.log(chalk.red(`"${skillPath}" is not a valid directory.`));
|
|
@@ -75,21 +80,136 @@ export async function addCommand(skillPath, options) {
|
|
|
75
80
|
console.log(chalk.red("No collections configured. Run: skillsmanager collection create"));
|
|
76
81
|
return;
|
|
77
82
|
}
|
|
83
|
+
// If the collection has metadata.repo (foreign skills repo), handle specially
|
|
84
|
+
if (collection.backend === "github") {
|
|
85
|
+
const github = new GithubBackend();
|
|
86
|
+
const col = await github.readCollection(collection);
|
|
87
|
+
const foreignRepo = col.metadata?.repo;
|
|
88
|
+
if (foreignRepo) {
|
|
89
|
+
const ctx = GithubBackend.detectRepoContext(absPath);
|
|
90
|
+
if (!ctx || ctx.repo !== foreignRepo) {
|
|
91
|
+
console.log(chalk.red(`This collection's skills source is "${foreignRepo}". ` +
|
|
92
|
+
`The provided path does not belong to that repo.\n` +
|
|
93
|
+
chalk.dim(` To register a skill by path without a local clone, use:\n`) +
|
|
94
|
+
chalk.dim(` skillsmanager add --collection ${collection.name} --remote-path <rel/path> --name <name> --description <desc>`)));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
// Path is from the foreign repo (cloned locally) — register relative path only, no upload
|
|
98
|
+
const spinner = ora(`Adding ${chalk.bold(skillName)} to ${collection.name}...`).start();
|
|
99
|
+
try {
|
|
100
|
+
const existing = col.skills.findIndex((s) => s.name === skillName);
|
|
101
|
+
const entry = { name: skillName, path: ctx.relPath, description };
|
|
102
|
+
if (existing >= 0) {
|
|
103
|
+
col.skills[existing] = entry;
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
col.skills.push(entry);
|
|
107
|
+
}
|
|
108
|
+
await github.writeCollection(collection, col);
|
|
109
|
+
trackSkill(skillName, collection.id, absPath);
|
|
110
|
+
spinner.succeed(`${chalk.bold(skillName)} registered in ${collection.name} at ${chalk.dim(ctx.relPath)}`);
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
spinner.fail(`Failed: ${err.message}`);
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
78
118
|
const backend = await resolveBackend(collection.backend);
|
|
79
119
|
await uploadToCollection(backend, collection, absPath, skillName, description);
|
|
80
120
|
}
|
|
121
|
+
// ── Remote-path mode: register a skill entry without local files ─────────────
|
|
122
|
+
async function addRemotePath(options) {
|
|
123
|
+
const { remotePath, name: skillName, description = "", collection: collectionName } = options;
|
|
124
|
+
if (!remotePath) {
|
|
125
|
+
console.log(chalk.red("--remote-path requires a relative path (e.g. tools/my-skill/)"));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (!skillName) {
|
|
129
|
+
console.log(chalk.red("--remote-path requires --name <skill-name>"));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
let config;
|
|
133
|
+
try {
|
|
134
|
+
config = readConfig();
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
console.log(chalk.red("No config found. Run: skillsmanager collection create"));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
let collection = config.collections[0];
|
|
141
|
+
if (collectionName) {
|
|
142
|
+
const found = config.collections.find((c) => c.name === collectionName);
|
|
143
|
+
if (!found) {
|
|
144
|
+
console.log(chalk.red(`Collection "${collectionName}" not found.`));
|
|
145
|
+
console.log(chalk.dim(` Available: ${config.collections.map((c) => c.name).join(", ")}`));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
collection = found;
|
|
149
|
+
}
|
|
150
|
+
if (!collection) {
|
|
151
|
+
console.log(chalk.red("No collections configured. Run: skillsmanager collection create"));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (collection.backend !== "github") {
|
|
155
|
+
console.log(chalk.red("--remote-path is only supported for GitHub collections."));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const github = new GithubBackend();
|
|
159
|
+
const spinner = ora(`Registering ${chalk.bold(skillName)} in ${collection.name} at ${chalk.dim(remotePath)}...`).start();
|
|
160
|
+
try {
|
|
161
|
+
const col = await github.readCollection(collection);
|
|
162
|
+
const existing = col.skills.findIndex((s) => s.name === skillName);
|
|
163
|
+
const entry = { name: skillName, path: remotePath, description };
|
|
164
|
+
if (existing >= 0) {
|
|
165
|
+
col.skills[existing] = entry;
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
col.skills.push(entry);
|
|
169
|
+
}
|
|
170
|
+
await github.writeCollection(collection, col);
|
|
171
|
+
spinner.succeed(`${chalk.bold(skillName)} registered in ${collection.name} at ${chalk.dim(remotePath)}`);
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
spinner.fail(`Failed: ${err.message}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
81
177
|
// ── GitHub path: register in-repo skill or copy external skill ────────────────
|
|
82
178
|
async function addToGithub(absPath, ctx, skillName, description, collection) {
|
|
83
179
|
const github = new GithubBackend();
|
|
84
180
|
const spinner = ora(`Adding ${chalk.bold(skillName)} to github:${collection.folderId}...`).start();
|
|
85
181
|
try {
|
|
182
|
+
const col = await github.readCollection(collection);
|
|
183
|
+
const foreignRepo = col.metadata?.repo;
|
|
184
|
+
const hostRepo = collection.folderId.split(":")[0];
|
|
185
|
+
// If collection has metadata.repo pointing to a foreign repo, validate that
|
|
186
|
+
// the local skill belongs to that foreign repo (not the collection host repo).
|
|
187
|
+
if (foreignRepo && foreignRepo !== hostRepo) {
|
|
188
|
+
if (ctx.repo !== foreignRepo) {
|
|
189
|
+
spinner.fail(`This collection's skills source is "${foreignRepo}" but the provided path belongs to "${ctx.repo}".`);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
// Skill is in the foreign repo (cloned locally) — register path only, no upload
|
|
193
|
+
const entry = { name: skillName, path: ctx.relPath, description };
|
|
194
|
+
const existing = col.skills.findIndex((s) => s.name === skillName);
|
|
195
|
+
if (existing >= 0) {
|
|
196
|
+
col.skills[existing] = entry;
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
col.skills.push(entry);
|
|
200
|
+
}
|
|
201
|
+
await github.writeCollection(collection, col);
|
|
202
|
+
trackSkill(skillName, collection.id, absPath);
|
|
203
|
+
spinner.succeed(`${chalk.bold(skillName)} registered in github:${collection.folderId} at ${chalk.dim(ctx.relPath)}`);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
// Standard case: skill is in (or being added to) the collection's host repo
|
|
86
207
|
// uploadSkill is a no-op for in-repo skills; copies if external
|
|
87
208
|
await github.uploadSkill(collection, absPath, skillName);
|
|
88
209
|
// Determine effective skill path in the repo
|
|
89
210
|
const skillEntry = absPath.startsWith(ctx.repoRoot)
|
|
90
211
|
? ctx.relPath // in-repo: use relative path
|
|
91
212
|
: `.agentskills/${skillName}`; // external: was copied here by uploadSkill
|
|
92
|
-
const col = await github.readCollection(collection);
|
|
93
213
|
const existing = col.skills.findIndex((s) => s.name === skillName);
|
|
94
214
|
if (existing >= 0) {
|
|
95
215
|
col.skills[existing] = { name: skillName, path: skillEntry, description };
|