@skillsmanager/cli 0.0.2 → 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 +54 -14
- package/dist/backends/gdrive.d.ts +2 -1
- package/dist/backends/gdrive.js +15 -4
- package/dist/backends/github.d.ts +27 -0
- package/dist/backends/github.js +407 -0
- package/dist/backends/interface.d.ts +2 -0
- package/dist/backends/local.d.ts +2 -0
- package/dist/backends/local.js +11 -0
- package/dist/backends/resolve.d.ts +2 -0
- package/dist/backends/resolve.js +11 -0
- package/dist/commands/add.d.ts +3 -0
- package/dist/commands/add.js +209 -7
- package/dist/commands/collection.d.ts +5 -1
- package/dist/commands/collection.js +99 -25
- package/dist/commands/fetch.js +7 -6
- package/dist/commands/list.js +5 -3
- package/dist/commands/logout.d.ts +4 -0
- package/dist/commands/logout.js +35 -0
- package/dist/commands/refresh.js +78 -25
- package/dist/commands/registry.d.ts +6 -0
- package/dist/commands/registry.js +200 -92
- package/dist/commands/setup/github.d.ts +4 -0
- package/dist/commands/setup/github.js +91 -0
- 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/commands/update.js +6 -4
- package/dist/index.js +57 -9
- package/dist/registry.js +11 -3
- package/dist/types.d.ts +1 -0
- package/dist/types.js +2 -0
- package/package.json +2 -2
- package/skills/skillsmanager/SKILL.md +49 -4
package/dist/commands/add.js
CHANGED
|
@@ -3,9 +3,15 @@ import path from "path";
|
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import ora from "ora";
|
|
5
5
|
import YAML from "yaml";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { readConfig, trackSkill } from "../config.js";
|
|
7
|
+
import { GithubBackend } from "../backends/github.js";
|
|
8
|
+
import { resolveBackend } from "../backends/resolve.js";
|
|
8
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
|
+
}
|
|
9
15
|
const absPath = path.resolve(skillPath);
|
|
10
16
|
if (!fs.existsSync(absPath) || !fs.statSync(absPath).isDirectory()) {
|
|
11
17
|
console.log(chalk.red(`"${skillPath}" is not a valid directory.`));
|
|
@@ -29,8 +35,37 @@ export async function addCommand(skillPath, options) {
|
|
|
29
35
|
console.log(chalk.red("SKILL.md frontmatter is missing 'name' field."));
|
|
30
36
|
return;
|
|
31
37
|
}
|
|
32
|
-
|
|
33
|
-
//
|
|
38
|
+
// ── Auto-detect if skill lives inside a GitHub-tracked repo ─────────────────
|
|
39
|
+
// If no --collection specified and a matching GitHub collection exists in config,
|
|
40
|
+
// use it automatically (no prompt — agent-friendly).
|
|
41
|
+
if (!options.collection) {
|
|
42
|
+
const ctx = GithubBackend.detectRepoContext(absPath);
|
|
43
|
+
if (ctx) {
|
|
44
|
+
let config;
|
|
45
|
+
try {
|
|
46
|
+
config = readConfig();
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
config = null;
|
|
50
|
+
}
|
|
51
|
+
const githubCollection = config?.collections.find((c) => c.backend === "github" && c.folderId.startsWith(`${ctx.repo}:`));
|
|
52
|
+
if (githubCollection) {
|
|
53
|
+
await addToGithub(absPath, ctx, skillName, description, githubCollection);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
// No matching GitHub collection — fall through to standard flow
|
|
57
|
+
// (user can run `skillsmanager collection create --backend github --repo <repo>` first)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// ── Standard flow ─────────────────────────────────────────────────────────────
|
|
61
|
+
let config;
|
|
62
|
+
try {
|
|
63
|
+
config = readConfig();
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
console.log(chalk.red("No config found. Run: skillsmanager collection create"));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
34
69
|
let collection = config.collections[0];
|
|
35
70
|
if (options.collection) {
|
|
36
71
|
const found = config.collections.find((c) => c.name === options.collection);
|
|
@@ -41,20 +76,187 @@ export async function addCommand(skillPath, options) {
|
|
|
41
76
|
}
|
|
42
77
|
collection = found;
|
|
43
78
|
}
|
|
79
|
+
if (!collection) {
|
|
80
|
+
console.log(chalk.red("No collections configured. Run: skillsmanager collection create"));
|
|
81
|
+
return;
|
|
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
|
+
}
|
|
118
|
+
const backend = await resolveBackend(collection.backend);
|
|
119
|
+
await uploadToCollection(backend, collection, absPath, skillName, description);
|
|
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
|
+
}
|
|
177
|
+
// ── GitHub path: register in-repo skill or copy external skill ────────────────
|
|
178
|
+
async function addToGithub(absPath, ctx, skillName, description, collection) {
|
|
179
|
+
const github = new GithubBackend();
|
|
180
|
+
const spinner = ora(`Adding ${chalk.bold(skillName)} to github:${collection.folderId}...`).start();
|
|
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
|
|
207
|
+
// uploadSkill is a no-op for in-repo skills; copies if external
|
|
208
|
+
await github.uploadSkill(collection, absPath, skillName);
|
|
209
|
+
// Determine effective skill path in the repo
|
|
210
|
+
const skillEntry = absPath.startsWith(ctx.repoRoot)
|
|
211
|
+
? ctx.relPath // in-repo: use relative path
|
|
212
|
+
: `.agentskills/${skillName}`; // external: was copied here by uploadSkill
|
|
213
|
+
const existing = col.skills.findIndex((s) => s.name === skillName);
|
|
214
|
+
if (existing >= 0) {
|
|
215
|
+
col.skills[existing] = { name: skillName, path: skillEntry, description };
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
col.skills.push({ name: skillName, path: skillEntry, description });
|
|
219
|
+
}
|
|
220
|
+
await github.writeCollection(collection, col);
|
|
221
|
+
trackSkill(skillName, collection.id, absPath);
|
|
222
|
+
spinner.succeed(`${chalk.bold(skillName)} registered in github:${collection.folderId} at ${chalk.dim(skillEntry)}`);
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
spinner.fail(`Failed: ${err.message}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// ── Shared: upload to any collection backend ──────────────────────────────────
|
|
229
|
+
async function uploadToCollection(backend, collection, absPath, skillName, description) {
|
|
44
230
|
const spinner = ora(`Adding ${chalk.bold(skillName)} to ${collection.name}...`).start();
|
|
45
231
|
try {
|
|
46
232
|
await backend.uploadSkill(collection, absPath, skillName);
|
|
233
|
+
// For GitHub collections, determine the effective in-repo path
|
|
234
|
+
let skillPath;
|
|
235
|
+
if (collection.backend === "github") {
|
|
236
|
+
// If the skill is already inside the repo workdir, use its relative path
|
|
237
|
+
const ctx = GithubBackend.detectRepoContext(absPath);
|
|
238
|
+
const repoFromCollection = collection.folderId.split(":")[0];
|
|
239
|
+
if (ctx && ctx.repo === repoFromCollection) {
|
|
240
|
+
skillPath = ctx.relPath; // e.g. "src/my-inrepo-skill"
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
skillPath = `.agentskills/${skillName}`; // external → copied here by uploadSkill
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
skillPath = `${skillName}/`;
|
|
248
|
+
}
|
|
47
249
|
const col = await backend.readCollection(collection);
|
|
48
250
|
const existing = col.skills.findIndex((s) => s.name === skillName);
|
|
49
251
|
if (existing >= 0) {
|
|
50
|
-
col.skills[existing] = { name: skillName, path:
|
|
252
|
+
col.skills[existing] = { name: skillName, path: skillPath, description };
|
|
51
253
|
}
|
|
52
254
|
else {
|
|
53
|
-
col.skills.push({ name: skillName, path:
|
|
255
|
+
col.skills.push({ name: skillName, path: skillPath, description });
|
|
54
256
|
}
|
|
55
257
|
await backend.writeCollection(collection, col);
|
|
56
258
|
trackSkill(skillName, collection.id, absPath);
|
|
57
|
-
spinner.succeed(`${chalk.bold(skillName)} added to
|
|
259
|
+
spinner.succeed(`${chalk.bold(skillName)} added to ${collection.backend}:${collection.name}`);
|
|
58
260
|
}
|
|
59
261
|
catch (err) {
|
|
60
262
|
spinner.fail(`Failed: ${err.message}`);
|
|
@@ -1,42 +1,116 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import ora from "ora";
|
|
3
|
-
import
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import { writeConfig, CONFIG_PATH, readConfig } from "../config.js";
|
|
4
5
|
import { ensureAuth } from "../auth.js";
|
|
5
6
|
import { GDriveBackend } from "../backends/gdrive.js";
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
import { GithubBackend } from "../backends/github.js";
|
|
8
|
+
import { LocalBackend } from "../backends/local.js";
|
|
9
|
+
import { resolveBackend } from "../backends/resolve.js";
|
|
10
|
+
export async function collectionCreateCommand(name, options = {}) {
|
|
11
|
+
const backendName = options.backend ?? "gdrive";
|
|
12
|
+
if (backendName === "github") {
|
|
13
|
+
await createGithubCollection(name, options.repo, options.skillsRepo);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
await createGdriveCollection(name);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async function createGithubCollection(name, repo, skillsRepo) {
|
|
20
|
+
if (!repo) {
|
|
21
|
+
console.log(chalk.red("GitHub backend requires --repo <owner/repo>"));
|
|
22
|
+
console.log(chalk.dim(" Example: skillsmanager collection create my-skills --backend github --repo owner/my-repo"));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const collectionName = name ?? "default";
|
|
26
|
+
const backend = new GithubBackend();
|
|
27
|
+
if (skillsRepo && skillsRepo !== repo) {
|
|
28
|
+
console.log(chalk.bold(`\nCreating GitHub collection "${collectionName}" in ${repo} (skills source: ${skillsRepo})...\n`));
|
|
14
29
|
}
|
|
15
30
|
else {
|
|
16
|
-
|
|
31
|
+
console.log(chalk.bold(`\nCreating GitHub collection "${collectionName}" in ${repo}...\n`));
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const collection = await backend.createCollection(collectionName, repo, skillsRepo);
|
|
35
|
+
console.log(chalk.green(`\n ✓ Collection "${collectionName}" created in github:${collection.folderId}`));
|
|
36
|
+
const config = loadOrDefaultConfig();
|
|
37
|
+
upsertCollection(config, collection);
|
|
38
|
+
const registry = await ensureRegistry(config);
|
|
39
|
+
await registerCollectionInRegistry(registry, collection, config);
|
|
40
|
+
writeConfig(config);
|
|
41
|
+
console.log(`\nRun ${chalk.bold("skillsmanager add <path>")} to add skills to it.\n`);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
console.log(chalk.red(`Failed: ${err.message}`));
|
|
17
45
|
}
|
|
46
|
+
}
|
|
47
|
+
async function createGdriveCollection(name) {
|
|
48
|
+
const auth = await ensureAuth();
|
|
49
|
+
const backend = new GDriveBackend(auth);
|
|
50
|
+
const PREFIX = "SKILLS_";
|
|
51
|
+
const folderName = !name
|
|
52
|
+
? `${PREFIX}MY_SKILLS`
|
|
53
|
+
: name.startsWith(PREFIX) ? name : `${PREFIX}${name}`;
|
|
18
54
|
const spinner = ora(`Creating collection "${folderName}" in Google Drive...`).start();
|
|
19
55
|
try {
|
|
20
56
|
const collection = await backend.createCollection(folderName);
|
|
21
57
|
spinner.succeed(`Collection "${folderName}" created in Google Drive`);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
catch { /* use default */ }
|
|
28
|
-
}
|
|
29
|
-
const already = config.collections.findIndex((c) => c.name === collection.name);
|
|
30
|
-
if (already >= 0) {
|
|
31
|
-
config.collections[already] = collection;
|
|
32
|
-
}
|
|
33
|
-
else {
|
|
34
|
-
config.collections.push(collection);
|
|
35
|
-
}
|
|
58
|
+
const config = loadOrDefaultConfig();
|
|
59
|
+
upsertCollection(config, collection);
|
|
60
|
+
const registry = await ensureRegistry(config);
|
|
61
|
+
await registerCollectionInRegistry(registry, collection, config);
|
|
36
62
|
writeConfig(config);
|
|
37
|
-
console.log(`\nRun ${chalk.bold(
|
|
63
|
+
console.log(`\nRun ${chalk.bold("skillsmanager add <path>")} to add skills to it.\n`);
|
|
38
64
|
}
|
|
39
65
|
catch (err) {
|
|
40
66
|
spinner.fail(`Failed: ${err.message}`);
|
|
41
67
|
}
|
|
42
68
|
}
|
|
69
|
+
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
70
|
+
function loadOrDefaultConfig() {
|
|
71
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
72
|
+
try {
|
|
73
|
+
return readConfig();
|
|
74
|
+
}
|
|
75
|
+
catch { /* fall through */ }
|
|
76
|
+
}
|
|
77
|
+
return { registries: [], collections: [], skills: {}, discoveredAt: new Date().toISOString() };
|
|
78
|
+
}
|
|
79
|
+
function upsertCollection(config, collection) {
|
|
80
|
+
const idx = config.collections.findIndex((c) => c.name === collection.name);
|
|
81
|
+
if (idx >= 0) {
|
|
82
|
+
config.collections[idx] = collection;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
config.collections.push(collection);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** Returns the first registry in config, auto-creating a local one if none exists. */
|
|
89
|
+
async function ensureRegistry(config) {
|
|
90
|
+
if (config.registries.length > 0)
|
|
91
|
+
return config.registries[0];
|
|
92
|
+
console.log(chalk.dim(" No registry found — creating a local registry..."));
|
|
93
|
+
const local = new LocalBackend();
|
|
94
|
+
const registry = await local.createRegistry();
|
|
95
|
+
config.registries.push(registry);
|
|
96
|
+
console.log(chalk.green(" ✓ Local registry created"));
|
|
97
|
+
return registry;
|
|
98
|
+
}
|
|
99
|
+
/** Registers the collection ref in the given registry (writes directly to the registry's backend). */
|
|
100
|
+
async function registerCollectionInRegistry(registry, collection, config) {
|
|
101
|
+
const backend = await resolveBackend(registry.backend);
|
|
102
|
+
const registryData = await backend.readRegistry(registry);
|
|
103
|
+
if (registryData.collections.find((c) => c.name === collection.name))
|
|
104
|
+
return;
|
|
105
|
+
registryData.collections.push({
|
|
106
|
+
name: collection.name,
|
|
107
|
+
backend: collection.backend,
|
|
108
|
+
ref: collection.folderId,
|
|
109
|
+
});
|
|
110
|
+
await backend.writeRegistry(registry, registryData);
|
|
111
|
+
// Keep local config registry list in sync
|
|
112
|
+
if (!config.registries.find((r) => r.id === registry.id)) {
|
|
113
|
+
config.registries.push(registry);
|
|
114
|
+
}
|
|
115
|
+
console.log(chalk.dim(` Registered in registry "${registry.name}" (${registry.backend})`));
|
|
116
|
+
}
|
package/dist/commands/fetch.js
CHANGED
|
@@ -2,21 +2,22 @@ import chalk from "chalk";
|
|
|
2
2
|
import ora from "ora";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { getCachePath, ensureCachePath, createSymlink } from "../cache.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { readConfig, trackSkill } from "../config.js";
|
|
6
|
+
import { resolveBackend } from "../backends/resolve.js";
|
|
7
7
|
export async function fetchCommand(names, options) {
|
|
8
8
|
if (names.length === 0) {
|
|
9
9
|
console.log(chalk.red("Please specify at least one skill name."));
|
|
10
10
|
console.log(chalk.dim(" Example: skillsmanager fetch pdf-skill --agent claude"));
|
|
11
11
|
return;
|
|
12
12
|
}
|
|
13
|
-
const
|
|
14
|
-
// Gather all skills across collections
|
|
13
|
+
const config = readConfig();
|
|
14
|
+
// Gather all skills across collections (resolve backend per collection)
|
|
15
15
|
const allSkills = [];
|
|
16
16
|
for (const collection of config.collections) {
|
|
17
|
+
const backend = await resolveBackend(collection.backend);
|
|
17
18
|
const col = await backend.readCollection(collection);
|
|
18
19
|
for (const entry of col.skills) {
|
|
19
|
-
allSkills.push({ entry, collection });
|
|
20
|
+
allSkills.push({ entry, collection, backend });
|
|
20
21
|
}
|
|
21
22
|
}
|
|
22
23
|
const scope = options.scope ?? "global";
|
|
@@ -31,7 +32,7 @@ export async function fetchCommand(names, options) {
|
|
|
31
32
|
try {
|
|
32
33
|
ensureCachePath(match.collection);
|
|
33
34
|
const cachePath = getCachePath(match.collection, name);
|
|
34
|
-
await backend.downloadSkill(match.collection, name, cachePath);
|
|
35
|
+
await match.backend.downloadSkill(match.collection, name, cachePath);
|
|
35
36
|
const { skillsDir, created } = createSymlink(name, cachePath, options.agent, scope, cwd);
|
|
36
37
|
trackSkill(name, match.collection.id, path.join(skillsDir, name));
|
|
37
38
|
spinner.succeed(`${chalk.bold(name)} → ${scope === "project" ? "project" : "global"} ${options.agent} skills`);
|
package/dist/commands/list.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import ora from "ora";
|
|
3
|
-
import {
|
|
3
|
+
import { readConfig } from "../config.js";
|
|
4
|
+
import { resolveBackend } from "../backends/resolve.js";
|
|
4
5
|
export async function getAllSkills() {
|
|
5
|
-
const
|
|
6
|
+
const config = readConfig();
|
|
6
7
|
const allSkills = [];
|
|
7
8
|
for (const collection of config.collections) {
|
|
9
|
+
const backend = await resolveBackend(collection.backend);
|
|
8
10
|
const col = await backend.readCollection(collection);
|
|
9
11
|
for (const entry of col.skills) {
|
|
10
12
|
allSkills.push({ entry, collection });
|
|
@@ -27,7 +29,7 @@ export async function listCommand() {
|
|
|
27
29
|
console.log(`\n ${chalk.dim("NAME".padEnd(maxName + 2))}${chalk.dim("DESCRIPTION".padEnd(maxDesc + 2))}${chalk.dim("SOURCE")}`);
|
|
28
30
|
console.log(` ${chalk.dim("-".repeat(maxName + maxDesc + 30))}`);
|
|
29
31
|
for (const s of skills.sort((a, b) => a.entry.name.localeCompare(b.entry.name))) {
|
|
30
|
-
console.log(` ${chalk.cyan(s.entry.name.padEnd(maxName + 2))}${s.entry.description.padEnd(maxDesc + 2)}${chalk.dim(
|
|
32
|
+
console.log(` ${chalk.cyan(s.entry.name.padEnd(maxName + 2))}${s.entry.description.padEnd(maxDesc + 2)}${chalk.dim(`${s.collection.backend}:${s.collection.name}`)}`);
|
|
31
33
|
}
|
|
32
34
|
console.log();
|
|
33
35
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { CREDENTIALS_PATH, TOKEN_PATH } from "../config.js";
|
|
4
|
+
import { spawnSync } from "child_process";
|
|
5
|
+
export async function logoutGoogleCommand(options) {
|
|
6
|
+
const removeAll = options.all;
|
|
7
|
+
let removed = false;
|
|
8
|
+
if (fs.existsSync(TOKEN_PATH)) {
|
|
9
|
+
fs.unlinkSync(TOKEN_PATH);
|
|
10
|
+
console.log(chalk.green(" ✓ Removed token.json (OAuth session cleared)"));
|
|
11
|
+
removed = true;
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
console.log(chalk.dim(" – token.json not found (already logged out)"));
|
|
15
|
+
}
|
|
16
|
+
if (removeAll) {
|
|
17
|
+
if (fs.existsSync(CREDENTIALS_PATH)) {
|
|
18
|
+
fs.unlinkSync(CREDENTIALS_PATH);
|
|
19
|
+
console.log(chalk.green(" ✓ Removed credentials.json (OAuth client cleared)"));
|
|
20
|
+
removed = true;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
console.log(chalk.dim(" – credentials.json not found"));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (removed) {
|
|
27
|
+
console.log(chalk.dim("\n Run skillsmanager setup google to set up again."));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function logoutGithubCommand() {
|
|
31
|
+
const r = spawnSync("gh", ["auth", "logout"], { stdio: "inherit" });
|
|
32
|
+
if (r.status !== 0) {
|
|
33
|
+
console.log(chalk.red(" gh auth logout failed. Try manually: gh auth logout"));
|
|
34
|
+
}
|
|
35
|
+
}
|
package/dist/commands/refresh.js
CHANGED
|
@@ -1,40 +1,93 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import ora from "ora";
|
|
3
|
-
import { writeConfig, mergeCollections, readConfig } from "../config.js";
|
|
4
|
-
import {
|
|
5
|
-
import { GDriveBackend } from "../backends/gdrive.js";
|
|
3
|
+
import { writeConfig, mergeCollections, mergeRegistries, readConfig } from "../config.js";
|
|
4
|
+
import { resolveBackend } from "../backends/resolve.js";
|
|
6
5
|
export async function refreshCommand() {
|
|
7
|
-
const spinner = ora("Discovering
|
|
6
|
+
const spinner = ora("Discovering registries...").start();
|
|
8
7
|
try {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
let existing = [];
|
|
8
|
+
let existingCollections = [];
|
|
9
|
+
let existingSkills = {};
|
|
10
|
+
let existingRegistries = [];
|
|
13
11
|
try {
|
|
14
|
-
|
|
12
|
+
const cfg = readConfig();
|
|
13
|
+
existingCollections = cfg.collections;
|
|
14
|
+
existingSkills = cfg.skills ?? {};
|
|
15
|
+
existingRegistries = cfg.registries ?? [];
|
|
15
16
|
}
|
|
16
17
|
catch { /* no existing config */ }
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
// Step 1: Discover registries across all backends
|
|
19
|
+
const backendsToScan = ["gdrive", "github", "local"];
|
|
20
|
+
const freshRegistries = [];
|
|
21
|
+
const skippedBackends = [];
|
|
22
|
+
for (const backendName of backendsToScan) {
|
|
23
|
+
try {
|
|
24
|
+
const backend = await resolveBackend(backendName);
|
|
25
|
+
const found = await backend.discoverRegistries();
|
|
26
|
+
freshRegistries.push(...found);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
skippedBackends.push(backendName);
|
|
30
|
+
}
|
|
21
31
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
32
|
+
if (skippedBackends.length > 0) {
|
|
33
|
+
console.log(chalk.dim(` Skipped (not configured): ${skippedBackends.join(", ")}`));
|
|
34
|
+
console.log(chalk.dim(` Run: skillsmanager setup ${skippedBackends[0]} to enable`));
|
|
35
|
+
}
|
|
36
|
+
const mergedRegistries = mergeRegistries(freshRegistries, existingRegistries);
|
|
37
|
+
spinner.text = `Found ${mergedRegistries.length} registry(ies). Resolving collections...`;
|
|
38
|
+
// Step 2: Resolve collections from each registry's refs
|
|
39
|
+
const freshCollections = [];
|
|
40
|
+
for (const registry of mergedRegistries) {
|
|
41
|
+
try {
|
|
42
|
+
const backend = await resolveBackend(registry.backend);
|
|
43
|
+
const registryFile = await backend.readRegistry(registry);
|
|
44
|
+
for (const ref of registryFile.collections) {
|
|
45
|
+
try {
|
|
46
|
+
const refBackend = await resolveBackend(ref.backend);
|
|
47
|
+
const colInfo = await refBackend.resolveCollectionRef(ref);
|
|
48
|
+
if (colInfo) {
|
|
49
|
+
freshCollections.push(colInfo);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
console.log(chalk.dim(`\n Warning: collection "${ref.name}" listed in registry "${registry.name}" could not be resolved`));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Skip unresolvable refs silently
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Skip registries that can't be read
|
|
62
|
+
}
|
|
26
63
|
}
|
|
27
|
-
|
|
28
|
-
|
|
64
|
+
// Deduplicate by folderId before merging (same collection may appear in multiple registries)
|
|
65
|
+
const seenFolderIds = new Set();
|
|
66
|
+
const dedupedCollections = freshCollections.filter((c) => {
|
|
67
|
+
if (seenFolderIds.has(c.folderId))
|
|
68
|
+
return false;
|
|
69
|
+
seenFolderIds.add(c.folderId);
|
|
70
|
+
return true;
|
|
71
|
+
});
|
|
72
|
+
const mergedCollections = mergeCollections(dedupedCollections, existingCollections);
|
|
73
|
+
writeConfig({
|
|
74
|
+
registries: mergedRegistries,
|
|
75
|
+
collections: mergedCollections,
|
|
76
|
+
skills: existingSkills,
|
|
77
|
+
discoveredAt: new Date().toISOString(),
|
|
78
|
+
});
|
|
29
79
|
spinner.stop();
|
|
30
|
-
if (
|
|
31
|
-
console.log(chalk.yellow("No
|
|
32
|
-
console.log(chalk.dim(" Run: skillsmanager
|
|
80
|
+
if (mergedRegistries.length === 0) {
|
|
81
|
+
console.log(chalk.yellow("No registries found."));
|
|
82
|
+
console.log(chalk.dim(" Run: skillsmanager registry create"));
|
|
33
83
|
}
|
|
34
84
|
else {
|
|
35
|
-
console.log(chalk.green(`Found ${
|
|
36
|
-
for (const
|
|
37
|
-
console.log(`
|
|
85
|
+
console.log(chalk.green(`Found ${mergedRegistries.length} registry(ies), ${mergedCollections.length} collection(s):`));
|
|
86
|
+
for (const r of mergedRegistries) {
|
|
87
|
+
console.log(` registry: ${r.backend}:${r.name}`);
|
|
88
|
+
}
|
|
89
|
+
for (const c of mergedCollections) {
|
|
90
|
+
console.log(` collection: ${c.backend}:${c.name}`);
|
|
38
91
|
}
|
|
39
92
|
}
|
|
40
93
|
console.log();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export declare function registryCreateCommand(options: {
|
|
2
2
|
backend?: string;
|
|
3
|
+
repo?: string;
|
|
3
4
|
}): Promise<void>;
|
|
4
5
|
export declare function registryListCommand(): Promise<void>;
|
|
5
6
|
export declare function registryDiscoverCommand(options: {
|
|
@@ -9,6 +10,11 @@ export declare function registryAddCollectionCommand(collectionName: string, opt
|
|
|
9
10
|
backend?: string;
|
|
10
11
|
ref?: string;
|
|
11
12
|
}): Promise<void>;
|
|
13
|
+
export declare function registryRemoveCollectionCommand(collectionName: string, options: {
|
|
14
|
+
delete?: boolean;
|
|
15
|
+
backend?: string;
|
|
16
|
+
}): Promise<void>;
|
|
12
17
|
export declare function registryPushCommand(options: {
|
|
13
18
|
backend?: string;
|
|
19
|
+
repo?: string;
|
|
14
20
|
}): Promise<void>;
|